diff options
668 files changed, 11710 insertions, 3994 deletions
diff --git a/.babelrc.js b/.babelrc.js index bfcc7d96634..1b05a67354e 100644 --- a/.babelrc.js +++ b/.babelrc.js @@ -18,6 +18,7 @@ const plugins = [ '@babel/plugin-syntax-import-meta', '@babel/plugin-proposal-class-properties', '@babel/plugin-proposal-json-strings', + '@babel/plugin-proposal-private-methods', ]; // add code coverage tooling if necessary diff --git a/.gitignore b/.gitignore index 65f61e1fade..0696dd217af 100644 --- a/.gitignore +++ b/.gitignore @@ -80,3 +80,4 @@ eslint-report.html package-lock.json /junit_*.xml /coverage-frontend/ +jsdoc/ diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 3d12f4142ba..68dea56a67d 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -315,6 +315,7 @@ cloud-native-image: variables: GIT_DEPTH: "1" cache: {} + when: always script: - gem install gitlab --no-document - CNG_PROJECT_PATH="gitlab-org/build/CNG" BUILD_TRIGGER_TOKEN=$CI_JOB_TOKEN ./scripts/trigger-build cng @@ -498,6 +499,22 @@ rspec-mysql: <<: *rspec-metadata-mysql parallel: 50 +.rspec-quarantine: &rspec-quarantine + script: + - export CACHE_CLASSES=true + - scripts/gitaly-test-spawn + - bin/rspec --color --format documentation --tag quarantine spec/ + +rspec-pg-quarantine: + <<: *rspec-metadata-pg + <<: *rspec-quarantine + allow_failure: true + +rspec-mysql-quarantine: + <<: *rspec-metadata-mysql + <<: *rspec-quarantine + allow_failure: true + static-analysis: <<: *dedicated-no-docs-no-db-pull-cache-job dependencies: @@ -787,6 +804,7 @@ qa:selectors: - bundle exec bin/qa Test::Sanity::Selectors .qa-frontend-node: &qa-frontend-node + <<: *dedicated-no-docs-no-db-pull-cache-job stage: test variables: NODE_OPTIONS: --max_old_space_size=3584 @@ -801,11 +819,6 @@ qa:selectors: - yarn install --frozen-lockfile --cache-folder .yarn-cache - date - yarn run webpack-prod - <<: *except-docs - -qa-frontend-node:6: - <<: *qa-frontend-node - image: node:6-alpine qa-frontend-node:8: <<: *qa-frontend-node @@ -853,6 +866,21 @@ lint:javascript:report: paths: - eslint-report.html +jsdoc: + <<: *dedicated-no-docs-pull-cache-job + stage: post-test + dependencies: + - compile-assets + before_script: [] + script: + - date + - yarn run jsdoc || true # ignore exit code + artifacts: + name: jsdoc + expire_in: 31d + paths: + - jsdoc/ + pages: <<: *dedicated-no-docs-no-db-pull-cache-job before_script: [] @@ -862,6 +890,7 @@ pages: - karma - gitlab:assets:compile - lint:javascript:report + - jsdoc script: - mv public/ .public/ - mkdir public/ @@ -871,6 +900,7 @@ pages: - mv webpack-report/ public/webpack-report/ || true - cp .public/assets/application-*.css public/application.css || true - cp .public/assets/application-*.css.gz public/application.css.gz || true + - mv jsdoc/ public/jsdoc/ || true artifacts: paths: - public diff --git a/.gitlab/issue_templates/Add style proposal.md b/.gitlab/issue_templates/Add style proposal.md new file mode 100644 index 00000000000..1a3be44bea0 --- /dev/null +++ b/.gitlab/issue_templates/Add style proposal.md @@ -0,0 +1,16 @@ +## Description of the proposal + +<!-- +Please describe the proposal and add a link to the source (for example, http://www.betterspecs.org/). +--> + +- [ ] Mention the proposal in the next backend weekly call and the #backend channel to encourage contribution +- [ ] Proceed with the proposal once 50% of the maintainers have weighed in, and 80% of the votes are :+1: +- [ ] Once approved, mention it again in the next backend weekly call and the #backend channel + + +/label ~"development guidelines" +/label ~"Style decision" +/label ~Documentation + +/cc @gitlab-org/maintainers/rails-backend diff --git a/.gitlab/issue_templates/Feature proposal.md b/.gitlab/issue_templates/Feature proposal.md index 639a236631d..4d4d3bfda15 100644 --- a/.gitlab/issue_templates/Feature proposal.md +++ b/.gitlab/issue_templates/Feature proposal.md @@ -4,7 +4,7 @@ ### Target audience -<!--- For whom are we doing this? Include either a persona from https://design.gitlab.com/getting-started/personas or define a specific company role. e.a. "Release Manager" or "Security Analyst" --> +<!--- For whom are we doing this? Include either a persona from https://design.gitlab.com/getting-started/personas or define a specific company role. e.a. "Release Manager" or "Security Analyst". Use the persona labels as well https://gitlab.com/groups/gitlab-org/-/labels?utf8=%E2%9C%93&subscribed=&search=persona%3A --> ### Further details @@ -12,12 +12,12 @@ ### Proposal -<!--- How are we going to solve the problem? --> +<!--- How are we going to solve the problem? Try to include the user journey! --> ### What does success look like, and how can we measure that? -<!--- If no way to measure success, link to an issue that will implement a way to measure this --> +<!--- Define both the success metrics and acceptance criteria. Note thet success metrics indicate the desired business outcomes, while acceptance criteria indicate when the solution is working correctly. If there is no way to measure success, link to an issue that will implement a way to measure this --> ### Links / references -/label ~"feature proposal" +/label ~feature diff --git a/.gitlab/issue_templates/Security Release.md b/.gitlab/issue_templates/Security Release.md new file mode 100644 index 00000000000..1734e915ad2 --- /dev/null +++ b/.gitlab/issue_templates/Security Release.md @@ -0,0 +1,69 @@ +<!-- +# Read me first! + +Set the title to: `Security Release: 11.4.X, 11.3.X, and 11.2.X` +--> + +## Releases tasks + +- https://gitlab.com/gitlab-org/release/docs/blob/master/general/security/release-manager.md +- https://gitlab.com/gitlab-org/release/docs/blob/master/general/security/developer.md +- https://gitlab.com/gitlab-org/release/docs/blob/master/general/security/security-engineer.md + +## Version issues: + +* 11.4.X: {release task link} +* 11.3.X: {release task link} +* 11.2.X: {release task link} + +## Security Issues: + +### CE + +* {https://gitlab.com/gitlab-org/gitlab-ce/issues link} + +### EE + +* {https://gitlab.com/gitlab-org/gitlab-ee/issues link} + +## Security Issues in dev.gitlab.org: + +### CE + +- {https://dev.gitlab.org/gitlab/gitlabhq/issues link} + +| Version | MR | Status| +|---------|----|-------| +| 11.4 | {https://dev.gitlab.org/gitlab/gitlabhq/merge_requests/ link} | | +| 11.3 | {https://dev.gitlab.org/gitlab/gitlabhq/merge_requests/ link} | | +| 11.2 | {https://dev.gitlab.org/gitlab/gitlabhq/merge_requests/ link} | | +| master | {https://dev.gitlab.org/gitlab/gitlabhq/merge_requests/ link} | | + + + +### EE + +* {https://dev.gitlab.org/gitlab/gitlabhq/issues/ link} + + +| Version | MR | Status| +|---------|----|-------| +| 11.4| {https://dev.gitlab.org/gitlab/gitlab-ee/merge_requests/ link} | | +| 11.3 | {https://dev.gitlab.org/gitlab/gitlab-ee/merge_requests/ link} | | +| 11.2 | {https://dev.gitlab.org/gitlab/gitlab-ee/merge_requests/ link} | | +| master | {https://dev.gitlab.org/gitlab/gitlab-ee/merge_requests/ link} | | + + +## QA +{QA issue link} + +## Blog post + +Dev: {https://dev.gitlab.org/gitlab/www-gitlab-com/merge_requests/ link}<br/> +gitlab.com: {https://gitlab.com/gitlab-com/www-gitlab-com/merge_requests/ link} + +## Email notification +{https://gitlab.com/gitlab-com/marketing/general/issues/ link} + +/label ~security +/confidential diff --git a/.gitlab/issue_templates/Security developer workflow.md b/.gitlab/issue_templates/Security developer workflow.md index 08651195d98..f9bf700f809 100644 --- a/.gitlab/issue_templates/Security developer workflow.md +++ b/.gitlab/issue_templates/Security developer workflow.md @@ -20,7 +20,7 @@ Set the title to: `[Security] Description of the original issue` #### Backports -- [ ] Once the MR is ready to be merged, create MRs targetting the last 3 releases +- [ ] 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. - [ ] 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 the branch `security-X-Y` from `X-Y-stable` if it doesn't exist (and make sure it's up to date with stable) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 847a0f74aa2..f2ba9fdb174 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -686,17 +686,6 @@ Style/TrailingUnderscoreVariable: - 'spec/lib/gitlab/etag_caching/middleware_spec.rb' - 'spec/services/quick_actions/interpret_service_spec.rb' -# Offense count: 5 -# Cop supports --auto-correct. -# Configuration parameters: ExactNameMatch, AllowPredicates, AllowDSLWriters, IgnoreClassMethods, Whitelist. -# Whitelist: to_ary, to_a, to_c, to_enum, to_h, to_hash, to_i, to_int, to_io, to_open, to_path, to_proc, to_r, to_regexp, to_str, to_s, to_sym -Style/TrivialAccessors: - Exclude: - - 'app/models/external_issue.rb' - - 'app/serializers/base_serializer.rb' - - 'lib/gitlab/auth/ldap/person.rb' - - 'lib/system_check/base_check.rb' - # Offense count: 4 # Cop supports --auto-correct. Style/UnlessElse: diff --git a/CHANGELOG.md b/CHANGELOG.md index e86c818298b..b47dc4e19ac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,24 @@ documentation](doc/development/changelog.md) for instructions on adding your own entry. +## 11.6.5 (2019-01-17) + +### Fixed (5 changes) + +- Add syntax highlighting to suggestion diff. !24156 +- Fix broken templated "Too many changes to show" text. !24282 +- Fix requests profiler in admin page not rendering HTML properly. !24291 +- Fix no avatar not showing in user selection box. !24346 +- Fixed diff suggestions removing dashes. + + +## 11.6.4 (2019-01-15) + +### Security (1 change) + +- Validate bundle files before unpacking them. + + ## 11.6.3 (2019-01-04) ### Fixed (1 change) diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index 0eed1a29efd..f88cf52e6ef 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -1.12.0 +1.13.0
\ No newline at end of file diff --git a/GITLAB_PAGES_VERSION b/GITLAB_PAGES_VERSION index 3a3cd8cc8b0..88c5fb891dc 100644 --- a/GITLAB_PAGES_VERSION +++ b/GITLAB_PAGES_VERSION @@ -1 +1 @@ -1.3.1 +1.4.0 @@ -112,7 +112,7 @@ gem 'seed-fu', '~> 2.3.7' # Markdown and HTML processing gem 'html-pipeline', '~> 2.8' -gem 'deckar01-task_list', '2.0.0' +gem 'deckar01-task_list', '2.0.1' gem 'gitlab-markup', '~> 1.6.5' gem 'github-markup', '~> 1.7.0', require: 'github/markup' gem 'redcarpet', '~> 3.4' @@ -160,12 +160,12 @@ gem 'acts-as-taggable-on', '~> 5.0' # Background jobs gem 'sidekiq', '~> 5.2.1' -gem 'sidekiq-cron', '~> 0.6.0' +gem 'sidekiq-cron', '~> 1.0' gem 'redis-namespace', '~> 1.6.0' gem 'gitlab-sidekiq-fetcher', '~> 0.4.0', require: 'sidekiq-reliable-fetch' # Cron Parser -gem 'rufus-scheduler', '~> 3.4' +gem 'fugit', '~> 1.1' # HTTP requests gem 'httparty', '~> 0.13.3' @@ -304,6 +304,12 @@ group :metrics do gem 'raindrops', '~> 0.18' end +group :tracing do + # OpenTracing + gem 'opentracing', '~> 0.4.3' + gem 'jaeger-client', '~> 0.10.0' +end + group :development do gem 'foreman', '~> 0.84.0' gem 'brakeman', '~> 4.2', require: false diff --git a/Gemfile.lock b/Gemfile.lock index aed88b5de47..53f9e97f082 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -108,7 +108,7 @@ GEM capybara-screenshot (1.0.22) capybara (>= 1.0, < 4) launchy - carrierwave (1.3.0) + carrierwave (1.3.1) activemodel (>= 4.0.0) activesupport (>= 4.0.0) mime-types (>= 1.16) @@ -143,7 +143,7 @@ GEM database_cleaner (1.7.0) debug_inspector (0.0.3) debugger-ruby_core_source (1.3.8) - deckar01-task_list (2.0.0) + deckar01-task_list (2.0.1) html-pipeline declarative (0.0.10) declarative-option (0.1.0) @@ -185,7 +185,7 @@ GEM erubi (1.7.1) erubis (2.7.0) escape_utils (1.2.1) - et-orbi (1.0.3) + et-orbi (1.1.7) tzinfo eventmachine (1.2.7) excon (0.62.0) @@ -258,6 +258,9 @@ GEM foreman (0.84.0) thor (~> 0.19.1) formatador (0.2.5) + fugit (1.1.7) + et-orbi (~> 1.1, >= 1.1.7) + raabro (~> 1.1) fuubar (2.2.0) rspec-core (~> 3.0) ruby-progressbar (~> 1.4) @@ -282,7 +285,7 @@ GEM gitlab-markup (1.6.5) gitlab-sidekiq-fetcher (0.4.0) sidekiq (~> 5) - gitlab-styles (2.4.1) + gitlab-styles (2.5.1) rubocop (~> 0.54.0) rubocop-gitlab-security (~> 0.1.0) rubocop-rspec (~> 1.19) @@ -389,6 +392,9 @@ GEM cause json ipaddress (0.8.3) + jaeger-client (0.10.0) + opentracing (~> 0.3) + thrift jira-ruby (1.4.1) activesupport multipart-post @@ -544,6 +550,7 @@ GEM activesupport nokogiri (>= 1.4.4) omniauth (~> 1.0) + opentracing (0.4.3) org-ruby (0.9.12) rubypants (~> 0.2) orm_adapter (0.5.0) @@ -606,6 +613,7 @@ GEM get_process_mem (~> 0.2) puma (>= 2.7, < 4) pyu-ruby-sasl (0.0.3.3) + raabro (1.1.6) rack (2.0.6) rack-accept (0.4.5) rack (>= 0.4) @@ -775,8 +783,6 @@ GEM rubyntlm (0.6.2) rubypants (0.2.0) rubyzip (1.2.2) - rufus-scheduler (3.4.0) - et-orbi (~> 1.0) rugged (0.27.5) safe_yaml (1.0.4) sanitize (4.6.6) @@ -820,8 +826,8 @@ GEM connection_pool (~> 2.2, >= 2.2.2) rack-protection (>= 1.5.0) redis (>= 3.3.5, < 5) - sidekiq-cron (0.6.0) - rufus-scheduler (>= 3.3.0) + sidekiq-cron (1.0.4) + fugit (~> 1.1) sidekiq (>= 4.2.1) signet (0.11.0) addressable (~> 2.3) @@ -868,6 +874,7 @@ GEM rack (>= 1, < 3) thor (0.19.4) thread_safe (0.3.6) + thrift (0.11.0.0) tilt (2.0.8) timecop (0.8.1) timfel-krb5-auth (0.8.3) @@ -974,7 +981,7 @@ DEPENDENCIES connection_pool (~> 2.0) creole (~> 0.5.0) database_cleaner (~> 1.7.0) - deckar01-task_list (= 2.0.0) + deckar01-task_list (= 2.0.1) device_detector devise (~> 4.4) devise-two-factor (~> 3.0.0) @@ -1003,6 +1010,7 @@ DEPENDENCIES fog-rackspace (~> 0.1.1) font-awesome-rails (~> 4.7) foreman (~> 0.84.0) + fugit (~> 1.1) fuubar (~> 2.2.0) gemojione (~> 3.3) gettext (~> 3.2.2) @@ -1037,6 +1045,7 @@ DEPENDENCIES httparty (~> 0.13.3) icalendar influxdb (~> 0.2) + jaeger-client (~> 0.10.0) jira-ruby (~> 1.4) jquery-atwho-rails (~> 1.3.2) js_regex (~> 2.2.1) @@ -1077,6 +1086,7 @@ DEPENDENCIES omniauth-shibboleth (~> 1.3.0) omniauth-twitter (~> 1.4) omniauth_crowd (~> 2.2.0) + opentracing (~> 0.4.3) org-ruby (~> 0.9.12) peek (~> 1.0.1) peek-gc (~> 0.0.2) @@ -1127,7 +1137,6 @@ DEPENDENCIES ruby-prof (~> 0.17.0) ruby-progressbar ruby_parser (~> 3.8) - rufus-scheduler (~> 3.4) rugged (~> 0.27) sanitize (~> 4.6) sass (~> 3.5) @@ -1141,7 +1150,7 @@ DEPENDENCIES sham_rack (~> 1.3.6) shoulda-matchers (~> 3.1.2) sidekiq (~> 5.2.1) - sidekiq-cron (~> 0.6.0) + sidekiq-cron (~> 1.0) simple_po_parser (~> 1.1.2) simplecov (~> 0.14.0) slack-notifier (~> 1.5.1) diff --git a/PROCESS.md b/PROCESS.md index f2eed5544fc..7fdac098807 100644 --- a/PROCESS.md +++ b/PROCESS.md @@ -56,7 +56,7 @@ Below we describe the contributing process to GitLab for two reasons: Several people from the [GitLab team][team] are helping community members to get their contributions accepted by meeting our [Definition of done][done]. -What you can expect from them is described at https://about.gitlab.com/roles/merge-request-coach/. +What you can expect from them is described at https://about.gitlab.com/job-families/expert/merge-request-coach/. ### Milestones on community contribution issues diff --git a/app/assets/javascripts/badges/components/badge_list_row.vue b/app/assets/javascripts/badges/components/badge_list_row.vue index 9051be1e102..cad5611c8c5 100644 --- a/app/assets/javascripts/badges/components/badge_list_row.vue +++ b/app/assets/javascripts/badges/components/badge_list_row.vue @@ -55,7 +55,7 @@ export default { :disabled="badge.isDeleting" class="btn btn-default append-right-8" type="button" - @click="editBadge(badge);" + @click="editBadge(badge)" > <icon :size="16" :aria-label="__('Edit')" name="pencil" /> </button> @@ -65,7 +65,7 @@ export default { type="button" data-toggle="modal" data-target="#delete-badge-modal" - @click="updateBadgeInModal(badge);" + @click="updateBadgeInModal(badge)" > <icon :size="16" :aria-label="__('Delete')" name="remove" /> </button> diff --git a/app/assets/javascripts/boards/components/board_card.vue b/app/assets/javascripts/boards/components/board_card.vue index 30fbdb9e97f..f569322ab70 100644 --- a/app/assets/javascripts/boards/components/board_card.vue +++ b/app/assets/javascripts/boards/components/board_card.vue @@ -86,7 +86,7 @@ export default { class="board-card" @mousedown="mouseDown" @mousemove="mouseMove" - @mouseup="showIssue($event);" + @mouseup="showIssue($event)" > <issue-card-inner :list="list" diff --git a/app/assets/javascripts/boards/components/board_new_issue.vue b/app/assets/javascripts/boards/components/board_new_issue.vue index 93bcb4e129e..28d96dab605 100644 --- a/app/assets/javascripts/boards/components/board_new_issue.vue +++ b/app/assets/javascripts/boards/components/board_new_issue.vue @@ -96,7 +96,7 @@ export default { <template> <div class="board-new-issue-form"> <div class="board-card"> - <form @submit="submit($event);"> + <form @submit="submit($event)"> <div v-if="error" class="flash-container"> <div class="flash-alert">An error occurred. Please try again.</div> </div> diff --git a/app/assets/javascripts/boards/components/issue_card_inner.vue b/app/assets/javascripts/boards/components/issue_card_inner.vue index 0f581c3d37d..90ab3a76342 100644 --- a/app/assets/javascripts/boards/components/issue_card_inner.vue +++ b/app/assets/javascripts/boards/components/issue_card_inner.vue @@ -184,7 +184,7 @@ export default { :title="label.description" class="badge color-label append-right-4 prepend-top-4" type="button" - @click="filterByLabel(label);" + @click="filterByLabel(label)" > {{ label.title }} </button> diff --git a/app/assets/javascripts/boards/components/modal/empty_state.vue b/app/assets/javascripts/boards/components/modal/empty_state.vue index defd857b92c..2a0008467c4 100644 --- a/app/assets/javascripts/boards/components/modal/empty_state.vue +++ b/app/assets/javascripts/boards/components/modal/empty_state.vue @@ -58,7 +58,7 @@ export default { v-if="activeTab === 'selected'" class="btn btn-default" type="button" - @click="changeTab('all');" + @click="changeTab('all')" > Open issues </button> diff --git a/app/assets/javascripts/boards/components/modal/footer.vue b/app/assets/javascripts/boards/components/modal/footer.vue index b1bc7d87086..d4afd9d59da 100644 --- a/app/assets/javascripts/boards/components/modal/footer.vue +++ b/app/assets/javascripts/boards/components/modal/footer.vue @@ -71,7 +71,7 @@ export default { <span class="inline add-issues-footer-to-list"> to list </span> <lists-dropdown /> </div> - <button class="btn btn-default float-right" type="button" @click="toggleModal(false);"> + <button class="btn btn-default float-right" type="button" @click="toggleModal(false)"> Cancel </button> </footer> diff --git a/app/assets/javascripts/boards/components/modal/header.vue b/app/assets/javascripts/boards/components/modal/header.vue index d0e285a149e..1f0961e02d8 100644 --- a/app/assets/javascripts/boards/components/modal/header.vue +++ b/app/assets/javascripts/boards/components/modal/header.vue @@ -58,7 +58,7 @@ export default { class="close" data-dismiss="modal" aria-label="Close" - @click="toggleModal(false);" + @click="toggleModal(false)" > <span aria-hidden="true">×</span> </button> diff --git a/app/assets/javascripts/boards/components/modal/list.vue b/app/assets/javascripts/boards/components/modal/list.vue index 878bb002c6c..e9ed2de859d 100644 --- a/app/assets/javascripts/boards/components/modal/list.vue +++ b/app/assets/javascripts/boards/components/modal/list.vue @@ -130,7 +130,7 @@ export default { <div :class="{ 'is-active': issue.selected }" class="board-card" - @click="toggleIssue($event, issue);" + @click="toggleIssue($event, issue)" > <issue-card-inner :issue="issue" :issue-link-base="issueLinkBase" :root-path="rootPath" /> <icon diff --git a/app/assets/javascripts/boards/components/modal/lists_dropdown.vue b/app/assets/javascripts/boards/components/modal/lists_dropdown.vue index 820d0679df5..3fbe8fe1be7 100644 --- a/app/assets/javascripts/boards/components/modal/lists_dropdown.vue +++ b/app/assets/javascripts/boards/components/modal/lists_dropdown.vue @@ -38,7 +38,7 @@ export default { :class="{ 'is-active': list.id == selected.id }" href="#" role="button" - @click.prevent="modal.selectedList = list;" + @click.prevent="modal.selectedList = list" > <span :style="{ backgroundColor: list.label.color }" class="dropdown-label-box"> </span> {{ list.title }} diff --git a/app/assets/javascripts/boards/components/modal/tabs.vue b/app/assets/javascripts/boards/components/modal/tabs.vue index 7b800a6ab97..2d2920e312e 100644 --- a/app/assets/javascripts/boards/components/modal/tabs.vue +++ b/app/assets/javascripts/boards/components/modal/tabs.vue @@ -21,12 +21,12 @@ export default { <div class="top-area prepend-top-10 append-bottom-10"> <ul class="nav-links issues-state-filters"> <li :class="{ active: activeTab == 'all' }"> - <a href="#" role="button" @click.prevent="changeTab('all');"> + <a href="#" role="button" @click.prevent="changeTab('all')"> Open issues <span class="badge badge-pill"> {{ issuesCount }} </span> </a> </li> <li :class="{ active: activeTab == 'selected' }"> - <a href="#" role="button" @click.prevent="changeTab('selected');"> + <a href="#" role="button" @click.prevent="changeTab('selected')"> Selected issues <span class="badge badge-pill"> {{ selectedCount }} </span> </a> </li> diff --git a/app/assets/javascripts/contextual_sidebar.js b/app/assets/javascripts/contextual_sidebar.js index 10f02739ec8..50efecb3475 100644 --- a/app/assets/javascripts/contextual_sidebar.js +++ b/app/assets/javascripts/contextual_sidebar.js @@ -13,6 +13,9 @@ export default class ContextualSidebar { initDomElements() { this.$page = $('.layout-page'); this.$sidebar = $('.nav-sidebar'); + + if (!this.$sidebar.length) return; + this.$innerScroll = $('.nav-sidebar-inner-scroll', this.$sidebar); this.$overlay = $('.mobile-overlay'); this.$openSidebar = $('.toggle-mobile-nav'); @@ -21,12 +24,14 @@ export default class ContextualSidebar { } bindEvents() { + if (!this.$sidebar.length) return; + document.addEventListener('click', e => { if ( !e.target.closest('.nav-sidebar') && (bp.getBreakpointSize() === 'sm' || bp.getBreakpointSize() === 'md') ) { - this.toggleCollapsedSidebar(true); + this.toggleCollapsedSidebar(true, true); } }); this.$openSidebar.on('click', () => this.toggleSidebarNav(true)); @@ -34,7 +39,7 @@ export default class ContextualSidebar { this.$overlay.on('click', () => this.toggleSidebarNav(false)); this.$sidebarToggle.on('click', () => { const value = !this.$sidebar.hasClass('sidebar-collapsed-desktop'); - this.toggleCollapsedSidebar(value); + this.toggleCollapsedSidebar(value, true); }); $(window).on('resize', () => _.debounce(this.render(), 100)); @@ -53,16 +58,19 @@ export default class ContextualSidebar { this.$sidebar.removeClass('sidebar-collapsed-desktop'); } - toggleCollapsedSidebar(collapsed) { + toggleCollapsedSidebar(collapsed, saveCookie) { const breakpoint = bp.getBreakpointSize(); if (this.$sidebar.length) { this.$sidebar.toggleClass('sidebar-collapsed-desktop', collapsed); this.$page.toggleClass('page-with-icon-sidebar', breakpoint === 'sm' ? true : collapsed); } - ContextualSidebar.setCollapsedCookie(collapsed); - this.toggleSidebarOverflow(); + if (saveCookie) { + ContextualSidebar.setCollapsedCookie(collapsed); + } + + requestIdleCallback(() => this.toggleSidebarOverflow()); } toggleSidebarOverflow() { @@ -74,13 +82,15 @@ export default class ContextualSidebar { } render() { + if (!this.$sidebar.length) return; + const breakpoint = bp.getBreakpointSize(); if (breakpoint === 'sm' || breakpoint === 'md') { - this.toggleCollapsedSidebar(true); + this.toggleCollapsedSidebar(true, false); } else if (breakpoint === 'lg') { const collapse = parseBoolean(Cookies.get('sidebar_collapsed')); - this.toggleCollapsedSidebar(collapse); + this.toggleCollapsedSidebar(collapse, false); } } } diff --git a/app/assets/javascripts/deploy_keys/components/key.vue b/app/assets/javascripts/deploy_keys/components/key.vue index f01e6f2a639..6ffb8c4e1c0 100644 --- a/app/assets/javascripts/deploy_keys/components/key.vue +++ b/app/assets/javascripts/deploy_keys/components/key.vue @@ -113,7 +113,7 @@ export default { <div class="gl-responsive-table-row deploy-key"> <div class="table-section section-40"> <div role="rowheader" class="table-mobile-header">{{ s__('DeployKeys|Deploy key') }}</div> - <div class="table-mobile-content"> + <div class="table-mobile-content qa-key"> <strong class="title qa-key-title"> {{ deployKey.title }} </strong> <div class="fingerprint qa-key-fingerprint">{{ deployKey.fingerprint }}</div> </div> diff --git a/app/assets/javascripts/diffs/components/compare_versions.vue b/app/assets/javascripts/diffs/components/compare_versions.vue index f75345d31f8..f0a827be7e8 100644 --- a/app/assets/javascripts/diffs/components/compare_versions.vue +++ b/app/assets/javascripts/diffs/components/compare_versions.vue @@ -54,6 +54,9 @@ export default { showDropdowns() { return !this.commit && this.mergeRequestDiffs.length; }, + baseVersionPath() { + return this.mergeRequestDiff.base_version_path; + }, }, methods: { ...mapActions('diffs', [ @@ -95,6 +98,7 @@ export default { and <compare-versions-dropdown :other-versions="comparableDiffs" + :base-version-path="baseVersionPath" :start-version="startVersion" :target-branch="targetBranch" class="mr-version-compare-dropdown" diff --git a/app/assets/javascripts/diffs/components/compare_versions_dropdown.vue b/app/assets/javascripts/diffs/components/compare_versions_dropdown.vue index b9b1ee02697..561188c1e8f 100644 --- a/app/assets/javascripts/diffs/components/compare_versions_dropdown.vue +++ b/app/assets/javascripts/diffs/components/compare_versions_dropdown.vue @@ -34,14 +34,13 @@ export default { required: false, default: false, }, + baseVersionPath: { + type: String, + required: false, + default: null, + }, }, computed: { - baseVersion() { - return { - name: 'hii', - versionIndex: -1, - }; - }, targetVersions() { if (this.mergeRequestVersion) { return this.otherVersions; @@ -62,6 +61,9 @@ export default { ); }, href(version) { + if (this.isBase(version)) { + return this.baseVersionPath; + } if (this.showCommitCount) { return version.version_path; } diff --git a/app/assets/javascripts/diffs/components/diff_content.vue b/app/assets/javascripts/diffs/components/diff_content.vue index ba6dcd63880..6dc2f5d3f68 100644 --- a/app/assets/javascripts/diffs/components/diff_content.vue +++ b/app/assets/javascripts/diffs/components/diff_content.vue @@ -127,7 +127,7 @@ export default { :save-button-title="__('Comment')" class="diff-comment-form new-note discussion-form discussion-form-container" @handleFormUpdate="handleSaveNote" - @cancelForm="closeDiffFileCommentForm(diffFile.file_hash);" + @cancelForm="closeDiffFileCommentForm(diffFile.file_hash)" /> </div> </diff-viewer> diff --git a/app/assets/javascripts/diffs/components/diff_discussions.vue b/app/assets/javascripts/diffs/components/diff_discussions.vue index b2021cd6061..4c73eea4049 100644 --- a/app/assets/javascripts/diffs/components/diff_discussions.vue +++ b/app/assets/javascripts/diffs/components/diff_discussions.vue @@ -68,7 +68,7 @@ export default { }" type="button" class="js-diff-notes-toggle" - @click="toggleDiscussion({ discussionId: discussion.id });" + @click="toggleDiscussion({ discussionId: discussion.id })" > <icon v-if="discussion.expanded" name="collapse" class="collapse-icon" /> <template v-else> diff --git a/app/assets/javascripts/diffs/components/diff_file_header.vue b/app/assets/javascripts/diffs/components/diff_file_header.vue index f75a01b023b..b58f704bebb 100644 --- a/app/assets/javascripts/diffs/components/diff_file_header.vue +++ b/app/assets/javascripts/diffs/components/diff_file_header.vue @@ -145,7 +145,7 @@ export default { <div ref="header" class="js-file-title file-title file-title-flex-parent" - @click="handleToggleFile($event, true);" + @click="handleToggleFile($event, true)" > <div class="file-header-content"> <icon diff --git a/app/assets/javascripts/diffs/components/diff_line_gutter_content.vue b/app/assets/javascripts/diffs/components/diff_line_gutter_content.vue index c0613d80d37..6709df48637 100644 --- a/app/assets/javascripts/diffs/components/diff_line_gutter_content.vue +++ b/app/assets/javascripts/diffs/components/diff_line_gutter_content.vue @@ -179,7 +179,7 @@ export default { v-if="lineNumber" :data-linenumber="lineNumber" :href="lineHref" - @click="setHighlightedRow(lineCode);" + @click="setHighlightedRow(lineCode)" > </a> <diff-gutter-avatars v-if="shouldShowAvatarsOnGutter" :discussions="line.discussions" /> diff --git a/app/assets/javascripts/diffs/components/diff_line_note_form.vue b/app/assets/javascripts/diffs/components/diff_line_note_form.vue index e7569ba7b84..18edbe286ba 100644 --- a/app/assets/javascripts/diffs/components/diff_line_note_form.vue +++ b/app/assets/javascripts/diffs/components/diff_line_note_form.vue @@ -28,6 +28,11 @@ export default { type: Object, required: true, }, + helpPagePath: { + type: String, + required: false, + default: '', + }, }, computed: { ...mapState({ @@ -95,6 +100,7 @@ export default { :is-editing="true" :line-code="line.line_code" :line="line" + :help-page-path="helpPagePath" save-button-title="Comment" class="diff-comment-form" @cancelForm="handleCancelCommentForm" diff --git a/app/assets/javascripts/diffs/components/image_diff_overlay.vue b/app/assets/javascripts/diffs/components/image_diff_overlay.vue index d30e64312aa..4a83c5a72a5 100644 --- a/app/assets/javascripts/diffs/components/image_diff_overlay.vue +++ b/app/assets/javascripts/diffs/components/image_diff_overlay.vue @@ -97,7 +97,7 @@ export default { v-if="canComment" type="button" class="btn-transparent position-absolute image-diff-overlay-add-comment w-100 h-100 js-add-image-diff-note-button" - @click="clickedImage($event.offsetX, $event.offsetY);" + @click="clickedImage($event.offsetX, $event.offsetY)" > <span class="sr-only"> {{ __('Add image comment') }} </span> </button> @@ -109,7 +109,7 @@ export default { :disabled="!shouldToggleDiscussion" class="js-image-badge" type="button" - @click="toggleDiscussion({ discussionId: discussion.id });" + @click="toggleDiscussion({ discussionId: discussion.id })" > <icon v-if="showCommentIcon" name="image-comment-dark" /> <template v-else> diff --git a/app/assets/javascripts/diffs/components/inline_diff_comment_row.vue b/app/assets/javascripts/diffs/components/inline_diff_comment_row.vue index 814ee0b7c02..69146f1f6fd 100644 --- a/app/assets/javascripts/diffs/components/inline_diff_comment_row.vue +++ b/app/assets/javascripts/diffs/components/inline_diff_comment_row.vue @@ -54,6 +54,7 @@ export default { :diff-file-hash="diffFileHash" :line="line" :note-target-line="line" + :help-page-path="helpPagePath" /> </div> </td> diff --git a/app/assets/javascripts/diffs/components/parallel_diff_comment_row.vue b/app/assets/javascripts/diffs/components/parallel_diff_comment_row.vue index a65cf025cde..370cb6e339a 100644 --- a/app/assets/javascripts/diffs/components/parallel_diff_comment_row.vue +++ b/app/assets/javascripts/diffs/components/parallel_diff_comment_row.vue @@ -101,6 +101,7 @@ export default { :diff-file-hash="diffFileHash" :line="line.left" :note-target-line="line.left" + :help-page-path="helpPagePath" line-position="left" /> </td> diff --git a/app/assets/javascripts/diffs/components/tree_list.vue b/app/assets/javascripts/diffs/components/tree_list.vue index eb8f274aff3..097587c5ac1 100644 --- a/app/assets/javascripts/diffs/components/tree_list.vue +++ b/app/assets/javascripts/diffs/components/tree_list.vue @@ -81,7 +81,7 @@ export default { :placeholder="s__('MergeRequest|Filter files')" type="search" class="form-control" - @focus="toggleFocusSearch(true);" + @focus="toggleFocusSearch(true)" @blur="blurSearch" /> <button @@ -104,7 +104,7 @@ export default { }" class="btn btn-default pt-0 pb-0 d-flex align-items-center" type="button" - @click="toggleRenderTreeList(false);" + @click="toggleRenderTreeList(false)" > <icon name="hamburger" /> </button> @@ -117,7 +117,7 @@ export default { }" class="btn btn-default pt-0 pb-0 d-flex align-items-center" type="button" - @click="toggleRenderTreeList(true);" + @click="toggleRenderTreeList(true)" > <icon name="file-tree" /> </button> diff --git a/app/assets/javascripts/environments/components/environment_actions.vue b/app/assets/javascripts/environments/components/environment_actions.vue index 1f7dab9fbd2..208bd19f6b0 100644 --- a/app/assets/javascripts/environments/components/environment_actions.vue +++ b/app/assets/javascripts/environments/components/environment_actions.vue @@ -92,7 +92,7 @@ export default { :disabled="isActionDisabled(action)" type="button" class="js-manual-action-link no-btn btn d-flex align-items-center" - @click="onClickAction(action);" + @click="onClickAction(action)" > <span class="flex-fill"> {{ action.name }} </span> <span v-if="action.scheduledAt" class="text-secondary"> diff --git a/app/assets/javascripts/environments/components/environments_app.vue b/app/assets/javascripts/environments/components/environments_app.vue index ae9459a2482..87c1d44dd40 100644 --- a/app/assets/javascripts/environments/components/environments_app.vue +++ b/app/assets/javascripts/environments/components/environments_app.vue @@ -96,9 +96,9 @@ export default { <tabs :tabs="tabs" scope="environments" @onChangeTab="onChangeTab" /> <div v-if="canCreateEnvironment && !isLoading" class="nav-controls"> - <a :href="newEnvironmentPath" class="btn btn-success"> - {{ s__('Environments|New environment') }} - </a> + <a :href="newEnvironmentPath" class="btn btn-success">{{ + s__('Environments|New environment') + }}</a> </div> </div> diff --git a/app/assets/javascripts/environments/components/environments_table.vue b/app/assets/javascripts/environments/components/environments_table.vue index 533e90e2222..75bdf87137f 100644 --- a/app/assets/javascripts/environments/components/environments_table.vue +++ b/app/assets/javascripts/environments/components/environments_table.vue @@ -85,9 +85,9 @@ export default { <div :key="`sub-div-${i}`"> <div class="text-center prepend-top-10"> - <a :href="folderUrl(model)" class="btn btn-default"> - {{ s__('Environments|Show all') }} - </a> + <a :href="folderUrl(model)" class="btn btn-default">{{ + s__('Environments|Show all') + }}</a> </div> </div> </template> diff --git a/app/assets/javascripts/environments/folder/environments_folder_bundle.js b/app/assets/javascripts/environments/folder/environments_folder_bundle.js index 3cf6e4ad14d..982e550e73c 100644 --- a/app/assets/javascripts/environments/folder/environments_folder_bundle.js +++ b/app/assets/javascripts/environments/folder/environments_folder_bundle.js @@ -15,11 +15,11 @@ export default () => const environmentsData = document.querySelector(this.$options.el).dataset; return { - endpoint: environmentsData.endpoint, - folderName: environmentsData.folderName, + endpoint: environmentsData.environmentsDataEndpoint, + folderName: environmentsData.environmentsDataFolderName, cssContainerClass: environmentsData.cssClass, - canCreateDeployment: parseBoolean(environmentsData.canCreateDeployment), - canReadEnvironment: parseBoolean(environmentsData.canReadEnvironment), + canCreateDeployment: parseBoolean(environmentsData.environmentsDataCanCreateDeployment), + canReadEnvironment: parseBoolean(environmentsData.environmentsDataCanReadEnvironment), }; }, render(createElement) { diff --git a/app/assets/javascripts/error_tracking/components/error_tracking_list.vue b/app/assets/javascripts/error_tracking/components/error_tracking_list.vue new file mode 100644 index 00000000000..6981afe1ead --- /dev/null +++ b/app/assets/javascripts/error_tracking/components/error_tracking_list.vue @@ -0,0 +1,118 @@ +<script> +import { mapActions, mapState } from 'vuex'; +import { GlEmptyState, GlButton, GlLink, GlLoadingIcon, GlTable } from '@gitlab/ui'; +import Icon from '~/vue_shared/components/icon.vue'; +import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue'; +import { __ } from '~/locale'; + +export default { + fields: [ + { key: 'error', label: __('Open errors') }, + { key: 'events', label: __('Events') }, + { key: 'users', label: __('Users') }, + { key: 'lastSeen', label: __('Last seen') }, + ], + components: { + GlEmptyState, + GlButton, + GlLink, + GlLoadingIcon, + GlTable, + Icon, + TimeAgo, + }, + props: { + indexPath: { + type: String, + required: true, + }, + enableErrorTrackingLink: { + type: String, + required: true, + }, + errorTrackingEnabled: { + type: Boolean, + required: true, + }, + illustrationPath: { + type: String, + required: true, + }, + }, + computed: { + ...mapState(['errors', 'externalUrl', 'loading']), + }, + created() { + if (this.errorTrackingEnabled) { + this.startPolling(this.indexPath); + } + }, + methods: { + ...mapActions(['startPolling']), + }, +}; +</script> + +<template> + <div> + <div v-if="errorTrackingEnabled"> + <div v-if="loading" class="py-3"><gl-loading-icon :size="3" /></div> + <div v-else> + <div class="d-flex justify-content-end"> + <gl-button class="my-3 ml-auto" variant="primary" :href="externalUrl" target="_blank" + >View in Sentry <icon name="external-link" /> + </gl-button> + </div> + <gl-table + :items="errors" + :fields="$options.fields" + :show-empty="true" + :empty-text="__('No errors to display')" + > + <template slot="HEAD_events" slot-scope="data"> + <div class="text-right">{{ data.label }}</div> + </template> + <template slot="HEAD_users" slot-scope="data"> + <div class="text-right">{{ data.label }}</div> + </template> + <template slot="error" slot-scope="errors"> + <div class="d-flex flex-column"> + <div class="d-flex"> + <gl-link :href="errors.item.externalUrl" class="d-flex text-dark" target="_blank"> + <strong>{{ errors.item.title.trim() }}</strong> + <icon name="external-link" class="ml-1" /> + </gl-link> + <span class="text-secondary ml-2">{{ errors.item.culprit }}</span> + </div> + {{ errors.item.message || __('No details available') }} + </div> + </template> + + <template slot="events" slot-scope="errors"> + <div class="text-right">{{ errors.item.count }}</div> + </template> + + <template slot="users" slot-scope="errors"> + <div class="text-right">{{ errors.item.userCount }}</div> + </template> + + <template slot="lastSeen" slot-scope="errors"> + <div class="d-flex align-items-center"> + <icon name="calendar" css-classes="text-secondary mr-1" /> + <time-ago :time="errors.item.lastSeen" class="text-secondary" /> + </div> + </template> + </gl-table> + </div> + </div> + <div v-else> + <gl-empty-state + :title="__('Get started with error tracking')" + :description="__('Monitor your errors by integrating with Sentry')" + :primary-button-text="__('Enable error tracking')" + :primary-button-link="enableErrorTrackingLink" + :svg-path="illustrationPath" + /> + </div> + </div> +</template> diff --git a/app/assets/javascripts/error_tracking/index.js b/app/assets/javascripts/error_tracking/index.js new file mode 100644 index 00000000000..808ae2c9a41 --- /dev/null +++ b/app/assets/javascripts/error_tracking/index.js @@ -0,0 +1,35 @@ +import Vue from 'vue'; +import { parseBoolean } from '~/lib/utils/common_utils'; +import store from './store'; +import ErrorTrackingList from './components/error_tracking_list.vue'; + +export default () => { + if (!gon.features.errorTracking) { + return; + } + + // eslint-disable-next-line no-new + new Vue({ + el: '#js-error_tracking', + components: { + ErrorTrackingList, + }, + store, + render(createElement) { + const domEl = document.querySelector(this.$options.el); + const { indexPath, enableErrorTrackingLink, illustrationPath } = domEl.dataset; + let { errorTrackingEnabled } = domEl.dataset; + + errorTrackingEnabled = parseBoolean(errorTrackingEnabled); + + return createElement('error-tracking-list', { + props: { + indexPath, + enableErrorTrackingLink, + errorTrackingEnabled, + illustrationPath, + }, + }); + }, + }); +}; diff --git a/app/assets/javascripts/error_tracking/services/index.js b/app/assets/javascripts/error_tracking/services/index.js new file mode 100644 index 00000000000..ab89521dc46 --- /dev/null +++ b/app/assets/javascripts/error_tracking/services/index.js @@ -0,0 +1,7 @@ +import axios from '~/lib/utils/axios_utils'; + +export default { + getErrorList({ endpoint }) { + return axios.get(endpoint); + }, +}; diff --git a/app/assets/javascripts/error_tracking/store/actions.js b/app/assets/javascripts/error_tracking/store/actions.js new file mode 100644 index 00000000000..2e192c958ba --- /dev/null +++ b/app/assets/javascripts/error_tracking/store/actions.js @@ -0,0 +1,31 @@ +import Service from '../services'; +import * as types from './mutation_types'; +import createFlash from '~/flash'; +import Poll from '~/lib/utils/poll'; +import { __ } from '~/locale'; + +let eTagPoll; + +export function startPolling({ commit }, endpoint) { + eTagPoll = new Poll({ + resource: Service, + method: 'getErrorList', + data: { endpoint }, + successCallback: ({ data }) => { + if (!data) { + return; + } + commit(types.SET_ERRORS, data.errors); + commit(types.SET_EXTERNAL_URL, data.external_url); + commit(types.SET_LOADING, false); + }, + errorCallback: () => { + commit(types.SET_LOADING, false); + createFlash(__('Failed to load errors from Sentry')); + }, + }); + + eTagPoll.makeRequest(); +} + +export default () => {}; diff --git a/app/assets/javascripts/error_tracking/store/index.js b/app/assets/javascripts/error_tracking/store/index.js new file mode 100644 index 00000000000..3136682fb64 --- /dev/null +++ b/app/assets/javascripts/error_tracking/store/index.js @@ -0,0 +1,19 @@ +import Vue from 'vue'; +import Vuex from 'vuex'; +import * as actions from './actions'; +import mutations from './mutations'; + +Vue.use(Vuex); + +export const createStore = () => + new Vuex.Store({ + state: { + errors: [], + externalUrl: '', + loading: true, + }, + actions, + mutations, + }); + +export default createStore(); diff --git a/app/assets/javascripts/error_tracking/store/mutation_types.js b/app/assets/javascripts/error_tracking/store/mutation_types.js new file mode 100644 index 00000000000..f9d77a6b08e --- /dev/null +++ b/app/assets/javascripts/error_tracking/store/mutation_types.js @@ -0,0 +1,3 @@ +export const SET_ERRORS = 'SET_ERRORS'; +export const SET_EXTERNAL_URL = 'SET_EXTERNAL_URL'; +export const SET_LOADING = 'SET_LOADING'; diff --git a/app/assets/javascripts/error_tracking/store/mutations.js b/app/assets/javascripts/error_tracking/store/mutations.js new file mode 100644 index 00000000000..e4bd81db9c9 --- /dev/null +++ b/app/assets/javascripts/error_tracking/store/mutations.js @@ -0,0 +1,14 @@ +import * as types from './mutation_types'; +import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; + +export default { + [types.SET_ERRORS](state, data) { + state.errors = convertObjectPropsToCamelCase(data, { deep: true }); + }, + [types.SET_EXTERNAL_URL](state, url) { + state.externalUrl = url; + }, + [types.SET_LOADING](state, loading) { + state.loading = loading; + }, +}; diff --git a/app/assets/javascripts/filtered_search/components/recent_searches_dropdown_content.vue b/app/assets/javascripts/filtered_search/components/recent_searches_dropdown_content.vue index 6b1a934d3fe..19bc3313373 100644 --- a/app/assets/javascripts/filtered_search/components/recent_searches_dropdown_content.vue +++ b/app/assets/javascripts/filtered_search/components/recent_searches_dropdown_content.vue @@ -66,7 +66,7 @@ export default { <button type="button" class="filtered-search-history-dropdown-item" - @click="onItemActivated(item.text);" + @click="onItemActivated(item.text)" > <span> <span @@ -88,7 +88,7 @@ export default { <button type="button" class="filtered-search-history-clear-button" - @click="onRequestClearRecentSearches($event);" + @click="onRequestClearRecentSearches($event)" > Clear recent searches </button> diff --git a/app/assets/javascripts/fly_out_nav.js b/app/assets/javascripts/fly_out_nav.js index 3ac00c51df4..2b6af9060d1 100644 --- a/app/assets/javascripts/fly_out_nav.js +++ b/app/assets/javascripts/fly_out_nav.js @@ -24,6 +24,9 @@ export const slope = (a, b) => (b.y - a.y) / (b.x - a.x); let headerHeight = 50; export const getHeaderHeight = () => headerHeight; +const setHeaderHeight = () => { + headerHeight = sidebar.offsetTop; +}; export const isSidebarCollapsed = () => sidebar && sidebar.classList.contains('sidebar-collapsed-desktop'); @@ -186,7 +189,7 @@ export default () => { }); } - headerHeight = document.querySelector('.nav-sidebar').offsetTop; + requestIdleCallback(setHeaderHeight); items.forEach(el => { const subItems = el.querySelector('.sidebar-sub-level-items'); diff --git a/app/assets/javascripts/header.js b/app/assets/javascripts/header.js index 2fa7a219ea8..3d846310008 100644 --- a/app/assets/javascripts/header.js +++ b/app/assets/javascripts/header.js @@ -23,7 +23,7 @@ export default function initTodoToggle() { }); } -document.addEventListener('DOMContentLoaded', () => { +function initStatusTriggers() { const setStatusModalTriggerEl = document.querySelector('.js-set-status-modal-trigger'); const setStatusModalWrapperEl = document.querySelector('.js-set-status-modal-wrapper'); @@ -72,4 +72,8 @@ document.addEventListener('DOMContentLoaded', () => { }, }); } +} + +document.addEventListener('DOMContentLoaded', () => { + requestIdleCallback(initStatusTriggers); }); diff --git a/app/assets/javascripts/ide/components/activity_bar.vue b/app/assets/javascripts/ide/components/activity_bar.vue index a1f66ff764d..7c769ab7fa0 100644 --- a/app/assets/javascripts/ide/components/activity_bar.vue +++ b/app/assets/javascripts/ide/components/activity_bar.vue @@ -45,7 +45,7 @@ export default { data-placement="right" type="button" class="ide-sidebar-link js-ide-edit-mode" - @click.prevent="changedActivityView($event, $options.activityBarViews.edit);" + @click.prevent="changedActivityView($event, $options.activityBarViews.edit)" > <icon name="code" /> </button> @@ -62,7 +62,7 @@ export default { data-placement="right" type="button" class="ide-sidebar-link js-ide-review-mode" - @click.prevent="changedActivityView($event, $options.activityBarViews.review);" + @click.prevent="changedActivityView($event, $options.activityBarViews.review)" > <icon name="file-modified" /> </button> @@ -79,7 +79,7 @@ export default { data-placement="right" type="button" class="ide-sidebar-link js-ide-commit-mode" - @click.prevent="changedActivityView($event, $options.activityBarViews.commit);" + @click.prevent="changedActivityView($event, $options.activityBarViews.commit)" > <icon name="commit" /> </button> diff --git a/app/assets/javascripts/ide/components/commit_sidebar/message_field.vue b/app/assets/javascripts/ide/components/commit_sidebar/message_field.vue index 6f1ded91753..00b2d236da3 100644 --- a/app/assets/javascripts/ide/components/commit_sidebar/message_field.vue +++ b/app/assets/javascripts/ide/components/commit_sidebar/message_field.vue @@ -111,8 +111,8 @@ export default { name="commit-message" @scroll="handleScroll" @input="onInput" - @focus="updateIsFocused(true);" - @blur="updateIsFocused(false);" + @focus="updateIsFocused(true)" + @blur="updateIsFocused(false)" @keydown.ctrl.enter="onCtrlEnter" @keydown.meta.enter="onCtrlEnter" > diff --git a/app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue b/app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue index 3525084b1cb..2b44438f849 100644 --- a/app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue +++ b/app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue @@ -65,7 +65,7 @@ export default { :disabled="disabled" type="radio" name="commit-action" - @change="updateCommitAction($event.target.value);" + @change="updateCommitAction($event.target.value)" /> <span class="prepend-left-10"> <span v-if="label" class="ide-radio-label"> {{ label }} </span> <slot v-else></slot> @@ -76,7 +76,7 @@ export default { :placeholder="newBranchName" type="text" class="form-control monospace" - @input="updateBranchName($event.target.value);" + @input="updateBranchName($event.target.value)" /> </div> </fieldset> diff --git a/app/assets/javascripts/ide/components/commit_sidebar/stage_button.vue b/app/assets/javascripts/ide/components/commit_sidebar/stage_button.vue index 02c2004d495..e054be86c1e 100644 --- a/app/assets/javascripts/ide/components/commit_sidebar/stage_button.vue +++ b/app/assets/javascripts/ide/components/commit_sidebar/stage_button.vue @@ -48,7 +48,7 @@ export default { data-container="body" data-boundary="viewport" data-placement="bottom" - @click.stop.prevent="stageChange(path);" + @click.stop.prevent="stageChange(path)" > <icon :size="16" name="mobile-issue-close" class="ml-auto mr-auto" /> </button> @@ -70,7 +70,7 @@ export default { :header-title-text="modalTitle" :footer-primary-button-text="__('Discard changes')" footer-primary-button-variant="danger" - @submit="discardFileChanges(path);" + @submit="discardFileChanges(path)" > {{ __("You will loose all changes you've made to this file. This action cannot be undone.") }} </gl-modal> diff --git a/app/assets/javascripts/ide/components/commit_sidebar/unstage_button.vue b/app/assets/javascripts/ide/components/commit_sidebar/unstage_button.vue index ce41fcdb087..0567ef54ff3 100644 --- a/app/assets/javascripts/ide/components/commit_sidebar/unstage_button.vue +++ b/app/assets/javascripts/ide/components/commit_sidebar/unstage_button.vue @@ -33,7 +33,7 @@ export default { data-container="body" data-boundary="viewport" data-placement="bottom" - @click.stop.prevent="unstageChange(path);" + @click.stop.prevent="unstageChange(path)" > <icon :size="16" name="redo" class="ml-auto mr-auto" /> </button> diff --git a/app/assets/javascripts/ide/components/editor_mode_dropdown.vue b/app/assets/javascripts/ide/components/editor_mode_dropdown.vue index 5f99261ec39..732fa0786b0 100644 --- a/app/assets/javascripts/ide/components/editor_mode_dropdown.vue +++ b/app/assets/javascripts/ide/components/editor_mode_dropdown.vue @@ -40,7 +40,7 @@ export default { 'is-active': viewer === $options.viewerTypes.mr, }" href="#" - @click.prevent="changeMode($options.viewerTypes.mr);" + @click.prevent="changeMode($options.viewerTypes.mr)" > <strong class="dropdown-menu-inner-title"> {{ mergeReviewLine }} </strong> <span class="dropdown-menu-inner-content"> @@ -54,7 +54,7 @@ export default { 'is-active': viewer === $options.viewerTypes.diff, }" href="#" - @click.prevent="changeMode($options.viewerTypes.diff);" + @click.prevent="changeMode($options.viewerTypes.diff)" > <strong class="dropdown-menu-inner-title">{{ __('Reviewing') }}</strong> <span class="dropdown-menu-inner-content"> diff --git a/app/assets/javascripts/ide/components/file_finder/index.vue b/app/assets/javascripts/ide/components/file_finder/index.vue index bb391912572..0b0cd7b75eb 100644 --- a/app/assets/javascripts/ide/components/file_finder/index.vue +++ b/app/assets/javascripts/ide/components/file_finder/index.vue @@ -164,7 +164,7 @@ export default { </script> <template> - <div class="ide-file-finder-overlay" @mousedown.self="toggleFileFinder(false);"> + <div class="ide-file-finder-overlay" @mousedown.self="toggleFileFinder(false)"> <div class="dropdown-menu diff-file-changes ide-file-finder show"> <div class="dropdown-input"> <input @@ -174,8 +174,8 @@ export default { type="search" class="dropdown-input-field" autocomplete="off" - @keydown="onKeydown($event);" - @keyup="onKeyup($event);" + @keydown="onKeydown($event)" + @keyup="onKeyup($event)" /> <i :class="{ diff --git a/app/assets/javascripts/ide/components/file_templates/dropdown.vue b/app/assets/javascripts/ide/components/file_templates/dropdown.vue index 414ea9c7d4d..343e0cca672 100644 --- a/app/assets/javascripts/ide/components/file_templates/dropdown.vue +++ b/app/assets/javascripts/ide/components/file_templates/dropdown.vue @@ -91,7 +91,7 @@ export default { <gl-loading-icon v-if="showLoading" :size="2" /> <ul v-else> <li v-for="(item, index) in outputData" :key="index"> - <button type="button" @click="clickItem(item);">{{ item.name }}</button> + <button type="button" @click="clickItem(item)">{{ item.name }}</button> </li> </ul> </div> diff --git a/app/assets/javascripts/ide/components/ide_status_bar.vue b/app/assets/javascripts/ide/components/ide_status_bar.vue index e2e0acc22b1..f1d40586903 100644 --- a/app/assets/javascripts/ide/components/ide_status_bar.vue +++ b/app/assets/javascripts/ide/components/ide_status_bar.vue @@ -84,7 +84,7 @@ export default { <button type="button" class="p-0 border-0 h-50" - @click="openRightPane($options.rightSidebarViews.pipelines);" + @click="openRightPane($options.rightSidebarViews.pipelines)" > <ci-icon v-tooltip diff --git a/app/assets/javascripts/ide/components/ide_tree.vue b/app/assets/javascripts/ide/components/ide_tree.vue index 9fc21adae7c..f93496132a4 100644 --- a/app/assets/javascripts/ide/components/ide_tree.vue +++ b/app/assets/javascripts/ide/components/ide_tree.vue @@ -43,7 +43,7 @@ export default { :show-label="false" class="d-flex border-0 p-0 mr-3 qa-new-file" icon="doc-new" - @click="openNewEntryModal({ type: 'blob' });" + @click="openNewEntryModal({ type: 'blob' })" /> <upload :show-label="false" @@ -56,7 +56,7 @@ export default { :show-label="false" class="d-flex border-0 p-0" icon="folder-new" - @click="openNewEntryModal({ type: 'tree' });" + @click="openNewEntryModal({ type: 'tree' })" /> </div> </template> diff --git a/app/assets/javascripts/ide/components/jobs/detail.vue b/app/assets/javascripts/ide/components/jobs/detail.vue index e8fe5fc696d..7710bfb49ec 100644 --- a/app/assets/javascripts/ide/components/jobs/detail.vue +++ b/app/assets/javascripts/ide/components/jobs/detail.vue @@ -75,7 +75,7 @@ export default { <template> <div class="ide-pipeline build-page d-flex flex-column flex-fill"> <header class="ide-job-header d-flex align-items-center"> - <button class="btn btn-default btn-sm d-flex" @click="setDetailJob(null);"> + <button class="btn btn-default btn-sm d-flex" @click="setDetailJob(null)"> <icon name="chevron-left" /> {{ __('View jobs') }} </button> </header> diff --git a/app/assets/javascripts/ide/components/merge_requests/list.vue b/app/assets/javascripts/ide/components/merge_requests/list.vue index ac2b0eddfb4..2d55ffb3c65 100644 --- a/app/assets/javascripts/ide/components/merge_requests/list.vue +++ b/app/assets/javascripts/ide/components/merge_requests/list.vue @@ -84,7 +84,7 @@ export default { :placeholder="__('Search merge requests')" @focus="onSearchFocus" @input="searchMergeRequests" - @removeToken="setSearchType(null);" + @removeToken="setSearchType(null)" /> <icon :size="18" name="search" class="input-icon" /> </div> @@ -102,7 +102,7 @@ export default { <button type="button" class="btn-link d-flex align-items-center" - @click.stop="setSearchType(searchType);" + @click.stop="setSearchType(searchType)" > <span class="d-flex append-right-default ide-search-list-current-icon"> <icon :size="18" name="search" /> diff --git a/app/assets/javascripts/ide/components/new_dropdown/index.vue b/app/assets/javascripts/ide/components/new_dropdown/index.vue index a50d729036f..d7a7b1b4d78 100644 --- a/app/assets/javascripts/ide/components/new_dropdown/index.vue +++ b/app/assets/javascripts/ide/components/new_dropdown/index.vue @@ -73,7 +73,7 @@ export default { :aria-label="__('Create new file or directory')" type="button" class="rounded border-0 d-flex ide-entry-dropdown-toggle" - @click.stop="openDropdown();" + @click.stop="openDropdown()" > <icon name="ellipsis_v" /> <icon name="arrow-down" /> </button> @@ -85,7 +85,7 @@ export default { class="d-flex" icon="doc-new" icon-classes="mr-2" - @click="createNewItem('blob');" + @click="createNewItem('blob')" /> </li> <li><upload :path="path" @create="createTempEntry" /></li> @@ -95,7 +95,7 @@ export default { class="d-flex" icon="folder-new" icon-classes="mr-2" - @click="createNewItem($options.modalTypes.tree);" + @click="createNewItem($options.modalTypes.tree)" /> </li> <li class="divider"></li> @@ -106,7 +106,7 @@ export default { class="d-flex" icon="pencil" icon-classes="mr-2" - @click="createNewItem($options.modalTypes.rename);" + @click="createNewItem($options.modalTypes.rename)" /> </li> <li> @@ -115,7 +115,7 @@ export default { class="d-flex" icon="remove" icon-classes="mr-2" - @click="deleteEntry(path);" + @click="deleteEntry(path)" /> </li> </ul> diff --git a/app/assets/javascripts/ide/components/new_dropdown/modal.vue b/app/assets/javascripts/ide/components/new_dropdown/modal.vue index 63cbf41b89b..04ecd4ba4e7 100644 --- a/app/assets/javascripts/ide/components/new_dropdown/modal.vue +++ b/app/assets/javascripts/ide/components/new_dropdown/modal.vue @@ -114,7 +114,7 @@ export default { <button type="button" class="btn btn-missing p-1 pr-2 pl-2" - @click="createFromTemplate(template);" + @click="createFromTemplate(template)" > {{ template.name }} </button> diff --git a/app/assets/javascripts/ide/components/panes/right.vue b/app/assets/javascripts/ide/components/panes/right.vue index 7a57ccf2dd3..2e6bd85feec 100644 --- a/app/assets/javascripts/ide/components/panes/right.vue +++ b/app/assets/javascripts/ide/components/panes/right.vue @@ -122,7 +122,7 @@ export default { data-placement="left" class="ide-sidebar-link is-right" type="button" - @click="clickTab($event, tab);" + @click="clickTab($event, tab)" > <icon :size="16" :name="tab.icon" /> </button> diff --git a/app/assets/javascripts/ide/components/repo_editor.vue b/app/assets/javascripts/ide/components/repo_editor.vue index c13d3ec094b..94a9e87369c 100644 --- a/app/assets/javascripts/ide/components/repo_editor.vue +++ b/app/assets/javascripts/ide/components/repo_editor.vue @@ -219,7 +219,7 @@ export default { <a href="javascript:void(0);" role="button" - @click.prevent="setFileViewMode({ file, viewMode: 'editor' });" + @click.prevent="setFileViewMode({ file, viewMode: 'editor' })" > <template v-if="viewer === $options.viewerTypes.edit"> {{ __('Edit') }} @@ -233,7 +233,7 @@ export default { <a href="javascript:void(0);" role="button" - @click.prevent="setFileViewMode({ file, viewMode: 'preview' });" + @click.prevent="setFileViewMode({ file, viewMode: 'preview' })" > {{ file.previewMode.previewTitle }} </a> diff --git a/app/assets/javascripts/ide/components/repo_tab.vue b/app/assets/javascripts/ide/components/repo_tab.vue index 4b87b83db8a..f6aa2295844 100644 --- a/app/assets/javascripts/ide/components/repo_tab.vue +++ b/app/assets/javascripts/ide/components/repo_tab.vue @@ -74,7 +74,7 @@ export default { active: tab.active, disabled: tab.pending, }" - @click="clickFile(tab);" + @click="clickFile(tab)" @mouseover="mouseOverTab" @mouseout="mouseOutTab" > @@ -88,7 +88,7 @@ export default { :disabled="tab.pending" type="button" class="multi-file-tab-close" - @click.stop.prevent="closeFile(tab);" + @click.stop.prevent="closeFile(tab)" > <icon v-if="!showChangedIcon" :size="12" name="close" /> <changed-file-icon v-else :file="tab" /> diff --git a/app/assets/javascripts/ide/components/resizable_panel.vue b/app/assets/javascripts/ide/components/resizable_panel.vue index a89de56ab5c..7277fcb7617 100644 --- a/app/assets/javascripts/ide/components/resizable_panel.vue +++ b/app/assets/javascripts/ide/components/resizable_panel.vue @@ -78,8 +78,8 @@ export default { :min-size="minSize" :max-size="$options.maxSize" :side="side === 'right' ? 'left' : 'right'" - @resize-start="setResizingStatus(true);" - @resize-end="setResizingStatus(false);" + @resize-start="setResizingStatus(true)" + @resize-end="setResizingStatus(false)" /> </div> </template> diff --git a/app/assets/javascripts/ide/components/shared/tokened_input.vue b/app/assets/javascripts/ide/components/shared/tokened_input.vue index f58e08c2cc9..de3e71dad92 100644 --- a/app/assets/javascripts/ide/components/shared/tokened_input.vue +++ b/app/assets/javascripts/ide/components/shared/tokened_input.vue @@ -76,8 +76,8 @@ export default { <button class="selectable btn-blank" type="button" - @click.stop="removeToken(token);" - @keyup.delete="removeToken(token);" + @click.stop="removeToken(token)" + @keyup.delete="removeToken(token)" > <div class="value-container rounded"> <div class="value">{{ token.label }}</div> diff --git a/app/assets/javascripts/ide/ide_router.js b/app/assets/javascripts/ide/ide_router.js index 3f6101e58f4..229ef168926 100644 --- a/app/assets/javascripts/ide/ide_router.js +++ b/app/assets/javascripts/ide/ide_router.js @@ -73,7 +73,7 @@ router.beforeEach((to, from, next) => { projectId: to.params.project, }) .then(() => { - const basePath = to.params[0] || ''; + const basePath = to.params.pathMatch || ''; const projectId = `${to.params.namespace}/${to.params.project}`; const branchId = to.params.branchid; const mergeRequestId = to.params.mrid; diff --git a/app/assets/javascripts/ide/index.js b/app/assets/javascripts/ide/index.js index 6351948f750..5a2b680c2f7 100644 --- a/app/assets/javascripts/ide/index.js +++ b/app/assets/javascripts/ide/index.js @@ -10,13 +10,20 @@ import { parseBoolean } from '../lib/utils/common_utils'; Vue.use(Translate); /** + * Function that receives the default store and returns an extended one. + * @callback extendStoreCallback + * @param {Vuex.Store} store + * @param {Element} el + */ + +/** * Initialize the IDE on the given element. * * @param {Element} el - The element that will contain the IDE. * @param {Object} options - Extra options for the IDE (Used by EE). * @param {Component} options.rootComponent - * Component that overrides the root component. - * @param {(store:Vuex.Store, el:Element) => Vuex.Store} options.extendStore - + * @param {extendStoreCallback} options.extendStore - * Function that receives the default store and returns an extended one. */ export function initIde(el, options = {}) { diff --git a/app/assets/javascripts/jobs/components/commit_block.vue b/app/assets/javascripts/jobs/components/commit_block.vue index e0f55518eef..7076a79dd5d 100644 --- a/app/assets/javascripts/jobs/components/commit_block.vue +++ b/app/assets/javascripts/jobs/components/commit_block.vue @@ -45,7 +45,7 @@ export default { /> <span v-if="mergeRequest"> - {{ __('in') }} + in <gl-link :href="mergeRequest.path" class="js-link-commit link-commit" >!{{ mergeRequest.iid }}</gl-link > diff --git a/app/assets/javascripts/jobs/components/job_app.vue b/app/assets/javascripts/jobs/components/job_app.vue index d2b7ce18290..d473d6a482d 100644 --- a/app/assets/javascripts/jobs/components/job_app.vue +++ b/app/assets/javascripts/jobs/components/job_app.vue @@ -80,7 +80,6 @@ export default { 'hasError', ]), ...mapGetters([ - 'headerActions', 'headerTime', 'shouldRenderCalloutMessage', 'shouldRenderTriggeredLabel', @@ -202,7 +201,6 @@ export default { :item-id="job.id" :time="headerTime" :user="job.user" - :actions="headerActions" :has-sidebar-button="true" :should-render-triggered-label="shouldRenderTriggeredLabel" :item-name="__('Job')" diff --git a/app/assets/javascripts/jobs/components/sidebar.vue b/app/assets/javascripts/jobs/components/sidebar.vue index ad3e7dabc79..a2141dc3760 100644 --- a/app/assets/javascripts/jobs/components/sidebar.vue +++ b/app/assets/javascripts/jobs/components/sidebar.vue @@ -48,8 +48,7 @@ export default { return `${this.job.runner.description} (#${this.job.runner.id})`; }, retryButtonClass() { - let className = - 'js-retry-button float-right btn btn-retry d-none d-md-block d-lg-block d-xl-block'; + let className = 'js-retry-button btn btn-retry'; className += this.job.status && this.job.recoverable ? ' btn-primary' : ' btn-inverted-secondary'; return className; @@ -110,24 +109,27 @@ export default { <aside class="right-sidebar build-sidebar" data-offset-top="101" data-spy="affix"> <div class="sidebar-container"> <div class="blocks-container"> - <div class="block d-flex align-items-center"> - <h4 class="flex-grow-1 prepend-top-8 m-0">{{ job.name }}</h4> - <gl-link - v-if="job.retry_path" - :class="retryButtonClass" - :href="job.retry_path" - data-method="post" - rel="nofollow" - >{{ __('Retry') }}</gl-link - > - <gl-link - v-if="job.terminal_path" - :href="job.terminal_path" - class="js-terminal-link pull-right btn btn-primary btn-inverted visible-md-block visible-lg-block" - target="_blank" - > - {{ __('Debug') }} <icon name="external-link" /> - </gl-link> + <div class="block d-flex flex-nowrap align-items-center"> + <h4 class="my-0 mr-2">{{ job.name }}</h4> + <div class="flex-grow-1 flex-shrink-0 text-right"> + <gl-link + v-if="job.retry_path" + :class="retryButtonClass" + :href="job.retry_path" + data-method="post" + rel="nofollow" + >{{ __('Retry') }}</gl-link + > + <gl-link + v-if="job.cancel_path" + :href="job.cancel_path" + class="js-cancel-job btn btn-default" + data-method="post" + rel="nofollow" + >{{ __('Cancel') }}</gl-link + > + </div> + <gl-button :aria-label="__('Toggle Sidebar')" type="button" @@ -137,22 +139,24 @@ export default { <i aria-hidden="true" data-hidden="true" class="fa fa-angle-double-right"></i> </gl-button> </div> - <div v-if="job.retry_path || job.new_issue_path" class="block retry-link"> + + <div v-if="job.terminal_path || job.new_issue_path" class="block retry-link"> <gl-link v-if="job.new_issue_path" :href="job.new_issue_path" - class="js-new-issue btn btn-success btn-inverted" + class="js-new-issue btn btn-success btn-inverted float-left mr-2" >{{ __('New issue') }}</gl-link > <gl-link - v-if="job.retry_path" - :href="job.retry_path" - class="js-retry-job btn btn-inverted-secondary" - data-method="post" - rel="nofollow" - >{{ __('Retry') }}</gl-link + v-if="job.terminal_path" + :href="job.terminal_path" + class="js-terminal-link btn btn-primary btn-inverted visible-md-block visible-lg-block float-left" + target="_blank" > + {{ __('Debug') }} <icon name="external-link" :size="14" /> + </gl-link> </div> + <div :class="{ block: renderBlock }"> <detail-row v-if="job.duration" @@ -193,16 +197,6 @@ export default { tag }}</span> </p> - - <div v-if="job.cancel_path" class="btn-group prepend-top-5" role="group"> - <gl-link - :href="job.cancel_path" - class="js-cancel-job btn btn-sm btn-default" - data-method="post" - rel="nofollow" - >{{ __('Cancel') }}</gl-link - > - </div> </div> <artifacts-block v-if="hasArtifact" :artifact="job.artifact" /> diff --git a/app/assets/javascripts/jobs/components/stages_dropdown.vue b/app/assets/javascripts/jobs/components/stages_dropdown.vue index 7f79e92067f..91332c21b52 100644 --- a/app/assets/javascripts/jobs/components/stages_dropdown.vue +++ b/app/assets/javascripts/jobs/components/stages_dropdown.vue @@ -55,7 +55,7 @@ export default { <ul class="dropdown-menu"> <li v-for="stage in stages" :key="stage.name"> - <button type="button" class="js-stage-item stage-item" @click="onStageClick(stage);"> + <button type="button" class="js-stage-item stage-item" @click="onStageClick(stage)"> {{ stage.name }} </button> </li> diff --git a/app/assets/javascripts/jobs/store/getters.js b/app/assets/javascripts/jobs/store/getters.js index 35e92b0b5d9..98911717381 100644 --- a/app/assets/javascripts/jobs/store/getters.js +++ b/app/assets/javascripts/jobs/store/getters.js @@ -1,22 +1,6 @@ import _ from 'underscore'; -import { __ } from '~/locale'; import { isScrolledToBottom } from '~/lib/utils/scroll_utils'; -export const headerActions = state => { - if (state.job.new_issue_path) { - return [ - { - label: __('New issue'), - path: state.job.new_issue_path, - cssClass: - 'js-new-issue btn btn-success btn-inverted d-none d-md-block d-lg-block d-xl-block', - type: 'link', - }, - ]; - } - return []; -}; - export const headerTime = state => (state.job.started ? state.job.started : state.job.created_at); export const shouldRenderCalloutMessage = state => diff --git a/app/assets/javascripts/layout_nav.js b/app/assets/javascripts/layout_nav.js index b8c3c237eb3..4314e5e1afb 100644 --- a/app/assets/javascripts/layout_nav.js +++ b/app/assets/javascripts/layout_nav.js @@ -11,48 +11,53 @@ function hideEndFade($scrollingTabs) { }); } +function initDeferred() { + $(document).trigger('init.scrolling-tabs'); +} + export default function initLayoutNav() { const contextualSidebar = new ContextualSidebar(); contextualSidebar.bindEvents(); initFlyOutNav(); - $(document) - .on('init.scrolling-tabs', () => { - const $scrollingTabs = $('.scrolling-tabs').not('.is-initialized'); - $scrollingTabs.addClass('is-initialized'); + // We need to init it on DomContentLoaded as others could also call it + $(document).on('init.scrolling-tabs', () => { + const $scrollingTabs = $('.scrolling-tabs').not('.is-initialized'); + $scrollingTabs.addClass('is-initialized'); - $(window) - .on('resize.nav', () => { - hideEndFade($scrollingTabs); - }) - .trigger('resize.nav'); + $(window) + .on('resize.nav', () => { + hideEndFade($scrollingTabs); + }) + .trigger('resize.nav'); - $scrollingTabs.on('scroll', function tabsScrollEvent() { - const $this = $(this); - const currentPosition = $this.scrollLeft(); - const maxPosition = $this.prop('scrollWidth') - $this.outerWidth(); + $scrollingTabs.on('scroll', function tabsScrollEvent() { + const $this = $(this); + const currentPosition = $this.scrollLeft(); + const maxPosition = $this.prop('scrollWidth') - $this.outerWidth(); - $this.siblings('.fade-left').toggleClass('scrolling', currentPosition > 0); - $this.siblings('.fade-right').toggleClass('scrolling', currentPosition < maxPosition - 1); - }); + $this.siblings('.fade-left').toggleClass('scrolling', currentPosition > 0); + $this.siblings('.fade-right').toggleClass('scrolling', currentPosition < maxPosition - 1); + }); - $scrollingTabs.each(function scrollTabsEachLoop() { - const $this = $(this); - const scrollingTabWidth = $this.width(); - const $active = $this.find('.active'); - const activeWidth = $active.width(); + $scrollingTabs.each(function scrollTabsEachLoop() { + const $this = $(this); + const scrollingTabWidth = $this.width(); + const $active = $this.find('.active'); + const activeWidth = $active.width(); - if ($active.length) { - const offset = $active.offset().left + activeWidth; + if ($active.length) { + const offset = $active.offset().left + activeWidth; - if (offset > scrollingTabWidth - 30) { - const scrollLeft = offset - scrollingTabWidth / 2 - activeWidth / 2; + if (offset > scrollingTabWidth - 30) { + const scrollLeft = offset - scrollingTabWidth / 2 - activeWidth / 2; - $this.scrollLeft(scrollLeft); - } + $this.scrollLeft(scrollLeft); } - }); - }) - .trigger('init.scrolling-tabs'); + } + }); + }); + + requestIdleCallback(initDeferred); } diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js index fc34d243dd7..3b6a57dad44 100644 --- a/app/assets/javascripts/lib/utils/common_utils.js +++ b/app/assets/javascripts/lib/utils/common_utils.js @@ -1,3 +1,7 @@ +/** + * @module common-utils + */ + import $ from 'jquery'; import axios from './axios_utils'; import { getLocationHash } from './url_utility'; @@ -426,13 +430,14 @@ export const historyPushState = newUrl => { }; /** - * Returns true for a String "true" and false otherwise. - * This is the opposite of Boolean(...).toString() + * Returns true for a String value of "true" and false otherwise. + * This is the opposite of Boolean(...).toString(). + * `parseBoolean` is idempotent. * * @param {String} value * @returns {Boolean} */ -export const parseBoolean = value => value === 'true'; +export const parseBoolean = value => (value && value.toString()) === 'true'; /** * Converts permission provided as strings to booleans. @@ -450,10 +455,16 @@ export const convertPermissionToBoolean = permission => { }; /** + * @callback backOffCallback + * @param {Function} next + * @param {Function} stop + */ + +/** * Back Off exponential algorithm * backOff :: (Function<next, stop>, Number) -> Promise<Any, Error> * - * @param {Function<next, stop>} fn function to be called + * @param {backOffCallback} fn function to be called * @param {Number} timeout * @return {Promise<Any, Error>} * @example diff --git a/app/assets/javascripts/lib/utils/url_utility.js b/app/assets/javascripts/lib/utils/url_utility.js index 9850f7ce782..4ba84589705 100644 --- a/app/assets/javascripts/lib/utils/url_utility.js +++ b/app/assets/javascripts/lib/utils/url_utility.js @@ -42,22 +42,35 @@ export function mergeUrlParams(params, url) { return `${urlparts[1]}?${query}${urlparts[3]}`; } -export function removeParamQueryString(url, param) { - const decodedUrl = decodeURIComponent(url); - const urlVariables = decodedUrl.split('&'); - - return urlVariables.filter(variable => variable.indexOf(param) === -1).join('&'); -} - -export function removeParams(params, source = window.location.href) { - const url = document.createElement('a'); - url.href = source; +/** + * Removes specified query params from the url by returning a new url string that no longer + * includes the param/value pair. If no url is provided, `window.location.href` is used as + * the default value. + * + * @param {string[]} params - the query param names to remove + * @param {string} [url=windowLocation().href] - url from which the query param will be removed + * @returns {string} A copy of the original url but without the query param + */ +export function removeParams(params, url = window.location.href) { + const [rootAndQuery, fragment] = url.split('#'); + const [root, query] = rootAndQuery.split('?'); + + if (!query) { + return url; + } - params.forEach(param => { - url.search = removeParamQueryString(url.search, param); - }); + const encodedParams = params.map(param => encodeURIComponent(param)); + const updatedQuery = query + .split('&') + .filter(paramPair => { + const [foundParam] = paramPair.split('='); + return encodedParams.indexOf(foundParam) < 0; + }) + .join('&'); - return url.href; + const writableQuery = updatedQuery.length > 0 ? `?${updatedQuery}` : ''; + const writableFragment = fragment ? `#${fragment}` : ''; + return `${root}${writableQuery}${writableFragment}`; } export function getLocationHash(url = window.location.href) { @@ -66,6 +79,20 @@ export function getLocationHash(url = window.location.href) { return hashIndex === -1 ? null : url.substring(hashIndex + 1); } +/** + * Apply the fragment to the given url by returning a new url string that includes + * the fragment. If the given url already contains a fragment, the original fragment + * will be removed. + * + * @param {string} url - url to which the fragment will be applied + * @param {string} fragment - fragment to append + */ +export const setUrlFragment = (url, fragment) => { + const [rootUrl] = url.split('#'); + const encodedFragment = encodeURIComponent(fragment.replace(/^#/, '')); + return `${rootUrl}#${encodedFragment}`; +}; + export function visitUrl(url, external = false) { if (external) { // Simulate `target="blank" rel="noopener noreferrer"` diff --git a/app/assets/javascripts/locale/sprintf.js b/app/assets/javascripts/locale/sprintf.js index 5246c49842e..68b64a3a16a 100644 --- a/app/assets/javascripts/locale/sprintf.js +++ b/app/assets/javascripts/locale/sprintf.js @@ -4,8 +4,8 @@ import _ from 'underscore'; Very limited implementation of sprintf supporting only named parameters. @param input (translated) text with parameters (e.g. '%{num_users} users use us') - @param parameters object mapping parameter names to values (e.g. { num_users: 5 }) - @param escapeParameters whether parameter values should be escaped (see http://underscorejs.org/#escape) + @param {Object} parameters object mapping parameter names to values (e.g. { num_users: 5 }) + @param {Boolean} escapeParameters whether parameter values should be escaped (see http://underscorejs.org/#escape) @returns {String} the text with parameters replaces (e.g. '5 users use us') @see https://ruby-doc.org/core-2.3.3/Kernel.html#method-i-sprintf diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index c866e8d180a..4ba3543f9b2 100644 --- a/app/assets/javascripts/main.js +++ b/app/assets/javascripts/main.js @@ -66,15 +66,11 @@ gl.lazyLoader = new LazyLoader({ observerNode: '#content-body', }); -document.addEventListener('DOMContentLoaded', () => { +// Put all initialisations here that can also wait after everything is rendered and ready +function deferredInitialisation() { const $body = $('body'); - const $document = $(document); - const $window = $(window); - const $sidebarGutterToggle = $('.js-sidebar-toggle'); - let bootstrapBreakpoint = bp.getBreakpointSize(); initBreadcrumbs(); - initLayoutNav(); initImporterStatus(); initTodoToggle(); initLogoAnimation(); @@ -84,34 +80,6 @@ document.addEventListener('DOMContentLoaded', () => { if (document.querySelector('.search')) initSearchAutocomplete(); if (document.querySelector('#js-peek')) initPerformanceBar({ container: '#js-peek' }); - // Set the default path for all cookies to GitLab's root directory - Cookies.defaults.path = gon.relative_url_root || '/'; - - // `hashchange` is not triggered when link target is already in window.location - $body.on('click', 'a[href^="#"]', function clickHashLinkCallback() { - const href = this.getAttribute('href'); - if (href.substr(1) === getLocationHash()) { - setTimeout(handleLocationHash, 1); - } - }); - - if (bootstrapBreakpoint === 'xs') { - const $rightSidebar = $('aside.right-sidebar, .layout-page'); - - $rightSidebar.removeClass('right-sidebar-expanded').addClass('right-sidebar-collapsed'); - } - - // prevent default action for disabled buttons - $('.btn').click(function clickDisabledButtonCallback(e) { - if ($(this).hasClass('disabled')) { - e.preventDefault(); - e.stopImmediatePropagation(); - return false; - } - - return true; - }); - addSelectOnFocusBehaviour('.js-select-on-focus'); $('.remove-row').on('ajax:success', function removeRowAjaxSuccessCallback() { @@ -164,6 +132,48 @@ document.addEventListener('DOMContentLoaded', () => { viewport: '.layout-page', }); + loadAwardsHandler(); +} + +document.addEventListener('DOMContentLoaded', () => { + const $body = $('body'); + const $document = $(document); + const $window = $(window); + const $sidebarGutterToggle = $('.js-sidebar-toggle'); + let bootstrapBreakpoint = bp.getBreakpointSize(); + + initLayoutNav(); + + // Set the default path for all cookies to GitLab's root directory + Cookies.defaults.path = gon.relative_url_root || '/'; + + // `hashchange` is not triggered when link target is already in window.location + $body.on('click', 'a[href^="#"]', function clickHashLinkCallback() { + const href = this.getAttribute('href'); + if (href.substr(1) === getLocationHash()) { + setTimeout(handleLocationHash, 1); + } + }); + + if (bootstrapBreakpoint === 'xs') { + const $rightSidebar = $('aside.right-sidebar, .layout-page'); + + $rightSidebar.removeClass('right-sidebar-expanded').addClass('right-sidebar-collapsed'); + } + + // prevent default action for disabled buttons + $('.btn').click(function clickDisabledButtonCallback(e) { + if ($(this).hasClass('disabled')) { + e.preventDefault(); + e.stopImmediatePropagation(); + return false; + } + + return true; + }); + + localTimeAgo($('abbr.timeago, .js-timeago'), true); + // Form submitter $('.trigger-submit').on('change', function triggerSubmitCallback() { $(this) @@ -171,8 +181,6 @@ document.addEventListener('DOMContentLoaded', () => { .submit(); }); - localTimeAgo($('abbr.timeago, .js-timeago'), true); - // Disable form buttons while a form is submitting $body.on('ajax:complete, ajax:beforeSend, submit', 'form', function ajaxCompleteCallback(e) { const $buttons = $('[type="submit"], .js-disable-on-submit', this); @@ -195,6 +203,10 @@ document.addEventListener('DOMContentLoaded', () => { } }); + $('.navbar-toggler').on('click', () => { + $('.header-content').toggleClass('menu-expanded'); + }); + // Commit show suppressed diff $document.on('click', '.diff-content .js-show-suppressed-diff', function showDiffCallback() { const $container = $(this).parent(); @@ -202,10 +214,6 @@ document.addEventListener('DOMContentLoaded', () => { $container.remove(); }); - $('.navbar-toggler').on('click', () => { - $('.header-content').toggleClass('menu-expanded'); - }); - // Show/hide comments on diff $body.on('click', '.js-toggle-diff-comments', function toggleDiffCommentsCallback(e) { const $this = $(this); @@ -250,8 +258,6 @@ document.addEventListener('DOMContentLoaded', () => { $window.on('resize.app', fitSidebarForSize); - loadAwardsHandler(); - $('form.filter-form').on('submit', function filterFormSubmitCallback(event) { const link = document.createElement('a'); link.href = this.action; @@ -274,4 +280,6 @@ document.addEventListener('DOMContentLoaded', () => { // initialize field errors $('.gl-show-field-errors').each((i, form) => new GlFieldErrors(form)); + + requestIdleCallback(deferredInitialisation); }); diff --git a/app/assets/javascripts/monitoring/components/graph.vue b/app/assets/javascripts/monitoring/components/graph.vue index 64a1df80a8e..309b73f5a4d 100644 --- a/app/assets/javascripts/monitoring/components/graph.vue +++ b/app/assets/javascripts/monitoring/components/graph.vue @@ -257,8 +257,8 @@ export default { <template> <div class="prometheus-graph" - @mouseover="showFlagContent = true;" - @mouseleave="showFlagContent = false;" + @mouseover="showFlagContent = true" + @mouseleave="showFlagContent = false" > <div class="prometheus-graph-header"> <h5 class="prometheus-graph-title">{{ graphData.title }}</h5> @@ -300,7 +300,7 @@ export default { :height="graphHeight - 100" class="prometheus-graph-overlay" transform="translate(-5, 20)" - @mousemove="handleMouseOverGraph($event);" + @mousemove="handleMouseOverGraph($event)" /> </svg> <svg v-else :viewBox="innerViewBox" class="js-no-data-to-display"> diff --git a/app/assets/javascripts/notebook/cells/code.vue b/app/assets/javascripts/notebook/cells/code.vue index bd6736152f5..eefc801ed7a 100644 --- a/app/assets/javascripts/notebook/cells/code.vue +++ b/app/assets/javascripts/notebook/cells/code.vue @@ -1,11 +1,12 @@ <script> -import CodeCell from './code/index.vue'; +import CodeOutput from './code/index.vue'; import OutputCell from './output/index.vue'; export default { + name: 'CodeCell', components: { - 'code-cell': CodeCell, - 'output-cell': OutputCell, + CodeOutput, + OutputCell, }, props: { cell: { @@ -29,8 +30,8 @@ export default { hasOutput() { return this.cell.outputs.length; }, - output() { - return this.cell.outputs[0]; + outputs() { + return this.cell.outputs; }, }, }; @@ -38,7 +39,7 @@ export default { <template> <div class="cell"> - <code-cell + <code-output :raw-code="rawInputCode" :count="cell.execution_count" :code-css-class="codeCssClass" @@ -47,7 +48,7 @@ export default { <output-cell v-if="hasOutput" :count="cell.execution_count" - :output="output" + :outputs="outputs" :code-css-class="codeCssClass" /> </div> diff --git a/app/assets/javascripts/notebook/cells/code/index.vue b/app/assets/javascripts/notebook/cells/code/index.vue index 8bf2431c4c6..98b6cdd0944 100644 --- a/app/assets/javascripts/notebook/cells/code/index.vue +++ b/app/assets/javascripts/notebook/cells/code/index.vue @@ -3,8 +3,9 @@ import Prism from '../../lib/highlight'; import Prompt from '../prompt.vue'; export default { + name: 'CodeOutput', components: { - prompt: Prompt, + Prompt, }, props: { count: { diff --git a/app/assets/javascripts/notebook/cells/output/html.vue b/app/assets/javascripts/notebook/cells/output/html.vue index c6fc786fa76..8dc2d73af9b 100644 --- a/app/assets/javascripts/notebook/cells/output/html.vue +++ b/app/assets/javascripts/notebook/cells/output/html.vue @@ -4,13 +4,21 @@ import Prompt from '../prompt.vue'; export default { components: { - prompt: Prompt, + Prompt, }, props: { + count: { + type: Number, + required: true, + }, rawCode: { type: String, required: true, }, + index: { + type: Number, + required: true, + }, }, computed: { sanitizedOutput() { @@ -21,13 +29,16 @@ export default { }, }); }, + showOutput() { + return this.index === 0; + }, }, }; </script> <template> <div class="output"> - <prompt /> + <prompt type="Out" :count="count" :show-output="showOutput" /> <div v-html="sanitizedOutput"></div> </div> </template> diff --git a/app/assets/javascripts/notebook/cells/output/image.vue b/app/assets/javascripts/notebook/cells/output/image.vue index fe8c81398fb..f1130275525 100644 --- a/app/assets/javascripts/notebook/cells/output/image.vue +++ b/app/assets/javascripts/notebook/cells/output/image.vue @@ -6,6 +6,10 @@ export default { prompt: Prompt, }, props: { + count: { + type: Number, + required: true, + }, outputType: { type: String, required: true, @@ -14,10 +18,24 @@ export default { type: String, required: true, }, + index: { + type: Number, + required: true, + }, + }, + computed: { + imgSrc() { + return `data:${this.outputType};base64,${this.rawCode}`; + }, + showOutput() { + return this.index === 0; + }, }, }; </script> <template> - <div class="output"><prompt /> <img :src="'data:' + outputType + ';base64,' + rawCode" /></div> + <div class="output"> + <prompt type="out" :count="count" :show-output="showOutput" /> <img :src="imgSrc" /> + </div> </template> diff --git a/app/assets/javascripts/notebook/cells/output/index.vue b/app/assets/javascripts/notebook/cells/output/index.vue index bd0bcc0d819..c5ae7e7ee10 100644 --- a/app/assets/javascripts/notebook/cells/output/index.vue +++ b/app/assets/javascripts/notebook/cells/output/index.vue @@ -1,14 +1,9 @@ <script> -import CodeCell from '../code/index.vue'; -import Html from './html.vue'; -import Image from './image.vue'; +import CodeOutput from '../code/index.vue'; +import HtmlOutput from './html.vue'; +import ImageOutput from './image.vue'; export default { - components: { - 'code-cell': CodeCell, - 'html-output': Html, - 'image-output': Image, - }, props: { codeCssClass: { type: String, @@ -20,68 +15,69 @@ export default { required: false, default: 0, }, - output: { - type: Object, + outputs: { + type: Array, required: true, - default: () => ({}), }, }, - computed: { - componentName() { - if (this.output.text) { - return 'code-cell'; - } else if (this.output.data['image/png']) { - return 'image-output'; - } else if (this.output.data['text/html']) { - return 'html-output'; - } else if (this.output.data['image/svg+xml']) { - return 'html-output'; - } + data() { + return { + outputType: '', + }; + }, + methods: { + dataForType(output, type) { + let data = output.data[type]; - return 'code-cell'; - }, - rawCode() { - if (this.output.text) { - return this.output.text.join(''); + if (typeof data === 'object') { + data = data.join(''); } - return this.dataForType(this.outputType); + return data; }, - outputType() { - if (this.output.text) { - return ''; - } else if (this.output.data['image/png']) { - return 'image/png'; - } else if (this.output.data['text/html']) { - return 'text/html'; - } else if (this.output.data['image/svg+xml']) { - return 'image/svg+xml'; + getComponent(output) { + if (output.text) { + return CodeOutput; + } else if (output.data['image/png']) { + this.outputType = 'image/png'; + + return ImageOutput; + } else if (output.data['text/html']) { + this.outputType = 'text/html'; + + return HtmlOutput; + } else if (output.data['image/svg+xml']) { + this.outputType = 'image/svg+xml'; + + return HtmlOutput; } - return 'text/plain'; + this.outputType = 'text/plain'; + return CodeOutput; }, - }, - methods: { - dataForType(type) { - let data = this.output.data[type]; - - if (typeof data === 'object') { - data = data.join(''); + rawCode(output) { + if (output.text) { + return output.text.join(''); } - return data; + return this.dataForType(output, this.outputType); }, }, }; </script> <template> - <component - :is="componentName" - :output-type="outputType" - :count="count" - :raw-code="rawCode" - :code-css-class="codeCssClass" - type="output" - /> + <div> + <component + :is="getComponent(output)" + v-for="(output, index) in outputs" + :key="index" + type="output" + :output-type="outputType" + :count="count" + :index="index" + :raw-code="rawCode(output)" + :code-css-class="codeCssClass" + /> + </div> </template> diff --git a/app/assets/javascripts/notebook/cells/prompt.vue b/app/assets/javascripts/notebook/cells/prompt.vue index 3f1f239a806..1eeb61844a4 100644 --- a/app/assets/javascripts/notebook/cells/prompt.vue +++ b/app/assets/javascripts/notebook/cells/prompt.vue @@ -11,18 +11,26 @@ export default { required: false, default: 0, }, + showOutput: { + type: Boolean, + required: false, + default: true, + }, }, computed: { hasKeys() { return this.type !== '' && this.count; }, + showTypeText() { + return this.type && this.count && this.showOutput; + }, }, }; </script> <template> <div class="prompt"> - <span v-if="hasKeys"> {{ type }} [{{ count }}]: </span> + <span v-if="showTypeText"> {{ type }} [{{ count }}]: </span> </div> </template> diff --git a/app/assets/javascripts/notebook/index.vue b/app/assets/javascripts/notebook/index.vue index 6a54d0b3823..e7056c03e4a 100644 --- a/app/assets/javascripts/notebook/index.vue +++ b/app/assets/javascripts/notebook/index.vue @@ -3,8 +3,8 @@ import { MarkdownCell, CodeCell } from './cells'; export default { components: { - 'code-cell': CodeCell, - 'markdown-cell': MarkdownCell, + CodeCell, + MarkdownCell, }, props: { notebook: { diff --git a/app/assets/javascripts/notes/components/comment_form.vue b/app/assets/javascripts/notes/components/comment_form.vue index 8bf02327cd2..d669ba5a8fa 100644 --- a/app/assets/javascripts/notes/components/comment_form.vue +++ b/app/assets/javascripts/notes/components/comment_form.vue @@ -357,9 +357,9 @@ js-gfm-input js-autosize markdown-area js-vue-textarea qa-comment-input" data-supports-quick-actions="true" aria-label="Description" placeholder="Write a comment or drag your files here…" - @keydown.up="editCurrentUserLastNote();" - @keydown.meta.enter="handleSave();" - @keydown.ctrl.enter="handleSave();" + @keydown.up="editCurrentUserLastNote()" + @keydown.meta.enter="handleSave()" + @keydown.ctrl.enter="handleSave()" > </textarea> </markdown-field> @@ -370,10 +370,10 @@ append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown" > <button :disabled="isSubmitButtonDisabled" - class="btn btn-create comment-btn js-comment-button js-comment-submit-button + class="btn btn-success js-comment-button js-comment-submit-button qa-comment-button" type="submit" - @click.prevent="handleSave();" + @click.prevent="handleSave()" > {{ __(commentButtonTitle) }} </button> @@ -381,7 +381,7 @@ append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown" :disabled="isSubmitButtonDisabled" name="button" type="button" - class="btn comment-btn note-type-toggle js-note-new-discussion dropdown-toggle qa-note-dropdown" + class="btn btn-success note-type-toggle js-note-new-discussion dropdown-toggle qa-note-dropdown" data-display="static" data-toggle="dropdown" aria-label="Open comment type dropdown" @@ -394,7 +394,7 @@ append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown" <button type="button" class="btn btn-transparent" - @click.prevent="setNoteType('comment');" + @click.prevent="setNoteType('comment')" > <i aria-hidden="true" class="fa fa-check icon"> </i> <div class="description"> @@ -408,7 +408,7 @@ append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown" <button type="button" class="btn btn-transparent qa-discussion-option" - @click.prevent="setNoteType('discussion');" + @click.prevent="setNoteType('discussion')" > <i aria-hidden="true" class="fa fa-check icon"> </i> <div class="description"> @@ -429,7 +429,7 @@ append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown" ]" :disabled="isToggleStateButtonLoading || isSubmitting" :label="issueActionButtonTitle" - @click="handleSave(true);" + @click="handleSave(true)" /> </div> </form> diff --git a/app/assets/javascripts/notes/components/discussion_filter.vue b/app/assets/javascripts/notes/components/discussion_filter.vue index f5c410211b6..e03d6e9cd02 100644 --- a/app/assets/javascripts/notes/components/discussion_filter.vue +++ b/app/assets/javascripts/notes/components/discussion_filter.vue @@ -1,6 +1,7 @@ <script> import $ from 'jquery'; import { mapGetters, mapActions } from 'vuex'; +import { getLocationHash } from '../../lib/utils/url_utility'; import Icon from '~/vue_shared/components/icon.vue'; import { DISCUSSION_FILTERS_DEFAULT_VALUE, @@ -44,29 +45,47 @@ export default { eventHub.$on('MergeRequestTabChange', this.toggleFilters); this.toggleFilters(currentTab); } + + window.addEventListener('hashchange', this.handleLocationHash); + this.handleLocationHash(); }, mounted() { this.toggleCommentsForm(); }, + destroyed() { + window.removeEventListener('hashchange', this.handleLocationHash); + }, methods: { - ...mapActions(['filterDiscussion', 'setCommentsDisabled']), + ...mapActions(['filterDiscussion', 'setCommentsDisabled', 'setTargetNoteHash']), selectFilter(value) { const filter = parseInt(value, 10); // close dropdown - $(this.$refs.dropdownToggle).dropdown('toggle'); + this.toggleDropdown(); if (filter === this.currentValue) return; this.currentValue = filter; this.filterDiscussion({ path: this.getNotesDataByProp('discussionsPath'), filter }); this.toggleCommentsForm(); }, + toggleDropdown() { + $(this.$refs.dropdownToggle).dropdown('toggle'); + }, toggleCommentsForm() { this.setCommentsDisabled(this.currentValue === HISTORY_ONLY_FILTER_VALUE); }, toggleFilters(tab) { this.displayFilters = tab === DISCUSSION_TAB_LABEL; }, + handleLocationHash() { + const hash = getLocationHash(); + + if (/^note_/.test(hash) && this.currentValue !== DISCUSSION_FILTERS_DEFAULT_VALUE) { + this.selectFilter(this.defaultValue); + this.toggleDropdown(); // close dropdown + this.setTargetNoteHash(hash); + } + }, }, }; </script> @@ -93,7 +112,7 @@ export default { :class="{ 'is-active': filter.value === currentValue }" class="qa-filter-options" type="button" - @click="selectFilter(filter.value);" + @click="selectFilter(filter.value)" > {{ filter.title }} </button> diff --git a/app/assets/javascripts/notes/components/note_awards_list.vue b/app/assets/javascripts/notes/components/note_awards_list.vue index 3d60eb02db8..3efdd1c5c17 100644 --- a/app/assets/javascripts/notes/components/note_awards_list.vue +++ b/app/assets/javascripts/notes/components/note_awards_list.vue @@ -1,6 +1,6 @@ <script> import { mapActions, mapGetters } from 'vuex'; -import { GlTooltipDirective } from '@gitlab/ui'; +import tooltip from '~/vue_shared/directives/tooltip'; import Icon from '~/vue_shared/components/icon.vue'; import Flash from '../../flash'; import { glEmojiTag } from '../../emoji'; @@ -10,7 +10,7 @@ export default { Icon, }, directives: { - GlTooltip: GlTooltipDirective, + tooltip, }, props: { awards: { @@ -167,19 +167,21 @@ export default { <button v-for="(awardList, awardName, index) in groupedAwards" :key="index" - v-gl-tooltip.bottom="{ boundary: 'viewport' }" + v-tooltip :class="getAwardClassBindings(awardList)" :title="awardTitle(awardList)" + data-boundary="viewport" + data-placement="bottom" class="btn award-control" type="button" - @click="handleAward(awardName);" + @click="handleAward(awardName)" > <span v-html="getAwardHTML(awardName)"></span> <span class="award-control-text js-counter">{{ awardList.length }}</span> </button> <div v-if="canAwardEmoji" class="award-menu-holder"> <button - v-gl-tooltip + v-tooltip :class="{ 'js-user-authored': isAuthoredByMe }" class="award-control btn js-add-award" title="Add reaction" diff --git a/app/assets/javascripts/notes/components/note_form.vue b/app/assets/javascripts/notes/components/note_form.vue index db62ddb3ecd..6dbb858e93d 100644 --- a/app/assets/javascripts/notes/components/note_form.vue +++ b/app/assets/javascripts/notes/components/note_form.vue @@ -219,10 +219,10 @@ export default { class="note-textarea js-gfm-input js-note-text js-autosize markdown-area js-vue-issue-note-form js-vue-textarea qa-reply-input" aria-label="Description" placeholder="Write a comment or drag your files here…" - @keydown.meta.enter="handleKeySubmit();" - @keydown.ctrl.enter="handleKeySubmit();" - @keydown.up="editMyLastNote();" - @keydown.esc="cancelHandler(true);" + @keydown.meta.enter="handleKeySubmit()" + @keydown.ctrl.enter="handleKeySubmit()" + @keydown.up="editMyLastNote()" + @keydown.esc="cancelHandler(true)" ></textarea> </markdown-field> <div class="note-form-actions clearfix"> @@ -230,21 +230,21 @@ export default { :disabled="isDisabled" type="button" class="js-vue-issue-save btn btn-success js-comment-button qa-reply-comment-button" - @click="handleUpdate();" + @click="handleUpdate()" > {{ saveButtonTitle }} </button> <button v-if="discussion.resolvable" class="btn btn-nr btn-default append-right-10 js-comment-resolve-button" - @click.prevent="handleUpdate(true);" + @click.prevent="handleUpdate(true)" > {{ resolveButtonTitle }} </button> <button class="btn btn-cancel note-edit-cancel js-close-discussion-note-form" type="button" - @click="cancelHandler();" + @click="cancelHandler()" > Cancel </button> diff --git a/app/assets/javascripts/notes/components/noteable_discussion.vue b/app/assets/javascripts/notes/components/noteable_discussion.vue index 7c3f5d00308..4480ec74182 100644 --- a/app/assets/javascripts/notes/components/noteable_discussion.vue +++ b/app/assets/javascripts/notes/components/noteable_discussion.vue @@ -440,7 +440,7 @@ Please check your network connection and try again.`; <button type="button" class="btn btn-default ml-sm-2" - @click="resolveHandler();" + @click="resolveHandler()" > <i v-if="isResolving" aria-hidden="true" class="fa fa-spinner fa-spin"></i> {{ resolveButtonTitle }} diff --git a/app/assets/javascripts/pages/projects/error_tracking/index.js b/app/assets/javascripts/pages/projects/error_tracking/index.js new file mode 100644 index 00000000000..5a8fe137e9a --- /dev/null +++ b/app/assets/javascripts/pages/projects/error_tracking/index.js @@ -0,0 +1,5 @@ +import ErrorTracking from '~/error_tracking'; + +document.addEventListener('DOMContentLoaded', () => { + ErrorTracking(); +}); diff --git a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue index db2a4041ec0..bd4309e47ad 100644 --- a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue +++ b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue @@ -70,7 +70,7 @@ export default { :checked="isEditable" class="label-bold" type="radio" - @click="toggleCustomInput(true);" + @click="toggleCustomInput(true)" /> <label for="custom"> {{ s__('PipelineSheduleIntervalPattern|Custom') }} </label> @@ -88,7 +88,7 @@ export default { :value="cronIntervalPresets.everyDay" class="label-bold" type="radio" - @click="toggleCustomInput(false);" + @click="toggleCustomInput(false)" /> <label class="label-bold" for="every-day"> {{ __('Every day (at 4:00am)') }} </label> @@ -102,7 +102,7 @@ export default { :value="cronIntervalPresets.everyWeek" class="label-bold" type="radio" - @click="toggleCustomInput(false);" + @click="toggleCustomInput(false)" /> <label class="label-bold" for="every-week"> @@ -118,7 +118,7 @@ export default { :value="cronIntervalPresets.everyMonth" class="label-bold" type="radio" - @click="toggleCustomInput(false);" + @click="toggleCustomInput(false)" /> <label class="label-bold" for="every-month"> diff --git a/app/assets/javascripts/pages/sessions/new/index.js b/app/assets/javascripts/pages/sessions/new/index.js index 07f32210d93..d54bff88f70 100644 --- a/app/assets/javascripts/pages/sessions/new/index.js +++ b/app/assets/javascripts/pages/sessions/new/index.js @@ -2,6 +2,7 @@ import $ from 'jquery'; import UsernameValidator from './username_validator'; import SigninTabsMemoizer from './signin_tabs_memoizer'; import OAuthRememberMe from './oauth_remember_me'; +import preserveUrlFragment from './preserve_url_fragment'; document.addEventListener('DOMContentLoaded', () => { new UsernameValidator(); // eslint-disable-line no-new @@ -10,4 +11,8 @@ document.addEventListener('DOMContentLoaded', () => { new OAuthRememberMe({ container: $('.omniauth-container'), }).bindEvents(); + + // Save the URL fragment from the current window location. This will be present if the user was + // redirected to sign-in after attempting to access a protected URL that included a fragment. + preserveUrlFragment(window.location.hash); }); diff --git a/app/assets/javascripts/pages/sessions/new/oauth_remember_me.js b/app/assets/javascripts/pages/sessions/new/oauth_remember_me.js index 761618109a4..191221a48cd 100644 --- a/app/assets/javascripts/pages/sessions/new/oauth_remember_me.js +++ b/app/assets/javascripts/pages/sessions/new/oauth_remember_me.js @@ -1,4 +1,5 @@ import $ from 'jquery'; +import { mergeUrlParams, removeParams } from '~/lib/utils/url_utility'; /** * OAuth-based login buttons have a separate "remember me" checkbox. @@ -24,9 +25,9 @@ export default class OAuthRememberMe { const href = $(element).attr('href'); if (rememberMe) { - $(element).attr('href', `${href}?remember_me=1`); + $(element).attr('href', mergeUrlParams({ remember_me: 1 }, href)); } else { - $(element).attr('href', href.replace('?remember_me=1', '')); + $(element).attr('href', removeParams(['remember_me'], href)); } }); } diff --git a/app/assets/javascripts/pages/sessions/new/preserve_url_fragment.js b/app/assets/javascripts/pages/sessions/new/preserve_url_fragment.js new file mode 100644 index 00000000000..e617fecaa0f --- /dev/null +++ b/app/assets/javascripts/pages/sessions/new/preserve_url_fragment.js @@ -0,0 +1,32 @@ +import { mergeUrlParams, setUrlFragment } from '~/lib/utils/url_utility'; + +/** + * Ensure the given URL fragment is preserved by appending it to sign-in/sign-up form actions and + * OAuth/SAML login links. + * + * @param fragment {string} - url fragment to be preserved + */ +export default function preserveUrlFragment(fragment = '') { + if (fragment) { + const normalFragment = fragment.replace(/^#/, ''); + + // Append the fragment to all sign-in/sign-up form actions so it is preserved when the user is + // eventually redirected back to the originally requested URL. + const forms = document.querySelectorAll('#signin-container form'); + Array.prototype.forEach.call(forms, form => { + const actionWithFragment = setUrlFragment(form.getAttribute('action'), `#${normalFragment}`); + form.setAttribute('action', actionWithFragment); + }); + + // Append a redirect_fragment query param to all oauth provider links. The redirect_fragment + // query param will be available in the omniauth callback upon successful authentication + const anchors = document.querySelectorAll('#signin-container a.oauth-login'); + Array.prototype.forEach.call(anchors, anchor => { + const newHref = mergeUrlParams( + { redirect_fragment: normalFragment }, + anchor.getAttribute('href'), + ); + anchor.setAttribute('href', newHref); + }); + } +} diff --git a/app/assets/javascripts/pages/users/user_overview_block.js b/app/assets/javascripts/pages/users/user_overview_block.js index e9ecec717d6..a7c3c9d104d 100644 --- a/app/assets/javascripts/pages/users/user_overview_block.js +++ b/app/assets/javascripts/pages/users/user_overview_block.js @@ -15,7 +15,8 @@ export default class UserOverviewBlock { } loadData() { - const loadingEl = document.querySelector(`${this.container} .loading`); + const containerEl = document.querySelector(this.container); + const loadingEl = containerEl.querySelector(`.loading`); loadingEl.classList.remove('hide'); @@ -42,7 +43,7 @@ export default class UserOverviewBlock { const nothingHereBlock = containerEl.querySelector('.nothing-here-block'); if (nothingHereBlock) { - nothingHereBlock.classList.add('text-left', 'p-0'); + nothingHereBlock.classList.add('p-5'); } } diff --git a/app/assets/javascripts/pipelines/components/graph/graph_component.vue b/app/assets/javascripts/pipelines/components/graph/graph_component.vue index 59cebaba717..a49dc311bd0 100644 --- a/app/assets/javascripts/pipelines/components/graph/graph_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/graph_component.vue @@ -1,59 +1,20 @@ <script> -import _ from 'underscore'; import { GlLoadingIcon } from '@gitlab/ui'; import StageColumnComponent from './stage_column_component.vue'; +import GraphMixin from '../../mixins/graph_component_mixin'; export default { components: { StageColumnComponent, GlLoadingIcon, }, - props: { - isLoading: { - type: Boolean, - required: true, - }, - pipeline: { - type: Object, - required: true, - }, - }, - computed: { - graph() { - return this.pipeline.details && this.pipeline.details.stages; - }, - }, - methods: { - capitalizeStageName(name) { - const escapedName = _.escape(name); - return escapedName.charAt(0).toUpperCase() + escapedName.slice(1); - }, - isFirstColumn(index) { - return index === 0; - }, - stageConnectorClass(index, stage) { - let className; - - // If it's the first stage column and only has one job - if (index === 0 && stage.groups.length === 1) { - className = 'no-margin'; - } else if (index > 0) { - // If it is not the first column - className = 'left-margin'; - } - - return className; - }, - refreshPipelineGraph() { - this.$emit('refreshPipelineGraph'); - }, - }, + mixins: [GraphMixin], }; </script> <template> <div class="build-content middle-block js-pipeline-graph"> <div class="pipeline-visualization pipeline-graph pipeline-tab-content"> - <div class="text-center"><gl-loading-icon v-if="isLoading" :size="3" /></div> + <div v-if="isLoading" class="m-auto"><gl-loading-icon :size="3" /></div> <ul v-if="!isLoading" class="stage-column-list"> <stage-column-component diff --git a/app/assets/javascripts/pipelines/components/pipelines_actions.vue b/app/assets/javascripts/pipelines/components/pipelines_actions.vue index 2e9f2519fcb..0152e2fbe04 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_actions.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_actions.vue @@ -77,7 +77,7 @@ export default { :class="{ disabled: isActionDisabled(action) }" :disabled="isActionDisabled(action)" class="js-pipeline-action-link no-btn btn" - @click="onClickAction(action);" + @click="onClickAction(action)" > {{ action.name }} <span v-if="action.scheduled_at" class="pull-right"> diff --git a/app/assets/javascripts/pipelines/components/stage.vue b/app/assets/javascripts/pipelines/components/stage.vue index 2d3f667e73e..7426936515a 100644 --- a/app/assets/javascripts/pipelines/components/stage.vue +++ b/app/assets/javascripts/pipelines/components/stage.vue @@ -172,8 +172,6 @@ export default { <span :aria-label="stage.title" aria-hidden="true" class="no-pointer-events"> <icon :name="borderlessIcon" /> </span> - - <i class="fa fa-caret-down" aria-hidden="true"> </i> </button> <div diff --git a/app/assets/javascripts/pipelines/mixins/graph_component_mixin.js b/app/assets/javascripts/pipelines/mixins/graph_component_mixin.js new file mode 100644 index 00000000000..66e9476dadf --- /dev/null +++ b/app/assets/javascripts/pipelines/mixins/graph_component_mixin.js @@ -0,0 +1,44 @@ +import _ from 'underscore'; + +export default { + props: { + isLoading: { + type: Boolean, + required: true, + }, + pipeline: { + type: Object, + required: true, + }, + }, + computed: { + graph() { + return this.pipeline.details && this.pipeline.details.stages; + }, + }, + methods: { + capitalizeStageName(name) { + const escapedName = _.escape(name); + return escapedName.charAt(0).toUpperCase() + escapedName.slice(1); + }, + isFirstColumn(index) { + return index === 0; + }, + stageConnectorClass(index, stage) { + let className; + + // If it's the first stage column and only has one job + if (index === 0 && stage.groups.length === 1) { + className = 'no-margin'; + } else if (index > 0) { + // If it is not the first column + className = 'left-margin'; + } + + return className; + }, + refreshPipelineGraph() { + this.$emit('refreshPipelineGraph'); + }, + }, +}; diff --git a/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_machine_type_dropdown.vue b/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_machine_type_dropdown.vue index 21095fcba16..83811ab489a 100644 --- a/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_machine_type_dropdown.vue +++ b/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_machine_type_dropdown.vue @@ -108,9 +108,7 @@ export default { </span> </li> <li v-for="result in results" :key="result.id"> - <button type="button" @click.prevent="setItem(result.name);"> - {{ result.name }} - </button> + <button type="button" @click.prevent="setItem(result.name)">{{ result.name }}</button> </li> </ul> </div> diff --git a/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_project_id_dropdown.vue b/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_project_id_dropdown.vue index 056584c8865..a2eb79af4f9 100644 --- a/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_project_id_dropdown.vue +++ b/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_project_id_dropdown.vue @@ -169,7 +169,7 @@ export default { </span> </li> <li v-for="result in results" :key="result.project_number"> - <button type="button" @click.prevent="setItem(result);">{{ result.name }}</button> + <button type="button" @click.prevent="setItem(result)">{{ result.name }}</button> </li> </ul> </div> diff --git a/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_zone_dropdown.vue b/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_zone_dropdown.vue index 728616a441f..5f8a4946f4a 100644 --- a/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_zone_dropdown.vue +++ b/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_zone_dropdown.vue @@ -82,9 +82,7 @@ export default { </span> </li> <li v-for="result in results" :key="result.id"> - <button type="button" @click.prevent="setItem(result.name);"> - {{ result.name }} - </button> + <button type="button" @click.prevent="setItem(result.name)">{{ result.name }}</button> </li> </ul> </div> diff --git a/app/assets/javascripts/registry/components/table_registry.vue b/app/assets/javascripts/registry/components/table_registry.vue index 78c7671856a..81fe0a48c06 100644 --- a/app/assets/javascripts/registry/components/table_registry.vue +++ b/app/assets/javascripts/registry/components/table_registry.vue @@ -70,7 +70,7 @@ export default { </thead> <tbody> <tr v-for="item in repo.list" :key="item.tag"> - <td> + <td class="monospace"> {{ item.tag }} <clipboard-button v-if="item.location" @@ -80,7 +80,9 @@ export default { /> </td> <td> - <span v-gl-tooltip.bottom :title="item.revision">{{ item.shortRevision }}</span> + <span v-gl-tooltip.bottom class="monospace" :title="item.revision">{{ + item.shortRevision + }}</span> </td> <td> {{ formatSize(item.size) }} @@ -104,7 +106,7 @@ export default { :aria-label="s__('ContainerRegistry|Remove tag')" variant="danger" class="js-delete-registry d-none d-sm-block float-right" - @click="handleDeleteRegistry(item);" + @click="handleDeleteRegistry(item)" > <icon name="remove" /> </gl-button> diff --git a/app/assets/javascripts/releases/components/app.vue b/app/assets/javascripts/releases/components/app.vue index 0ad5ee2915c..5a06c4fec58 100644 --- a/app/assets/javascripts/releases/components/app.vue +++ b/app/assets/javascripts/releases/components/app.vue @@ -1,12 +1,12 @@ <script> import { mapState, mapActions } from 'vuex'; -import { GlLoadingIcon, GlEmptyState } from '@gitlab/ui'; +import { GlSkeletonLoading, GlEmptyState } from '@gitlab/ui'; import ReleaseBlock from './release_block.vue'; export default { name: 'ReleasesApp', components: { - GlLoadingIcon, + GlSkeletonLoading, GlEmptyState, ReleaseBlock, }, @@ -43,7 +43,7 @@ export default { </script> <template> <div class="prepend-top-default"> - <gl-loading-icon v-if="isLoading" :size="2" class="js-loading prepend-top-20" /> + <gl-skeleton-loading v-if="isLoading" class="js-loading" /> <gl-empty-state v-else-if="shouldRenderEmptyState" diff --git a/app/assets/javascripts/reports/components/modal_open_name.vue b/app/assets/javascripts/reports/components/modal_open_name.vue index 118e4b02c46..4f81cee2a38 100644 --- a/app/assets/javascripts/reports/components/modal_open_name.vue +++ b/app/assets/javascripts/reports/components/modal_open_name.vue @@ -26,7 +26,7 @@ export default { <button type="button" class="btn-link btn-blank text-left break-link vulnerability-name-button" - @click="handleIssueClick();" + @click="handleIssueClick()" > {{ issue.title }} </button> diff --git a/app/assets/javascripts/reports/components/test_issue_body.vue b/app/assets/javascripts/reports/components/test_issue_body.vue index 938e83de546..7700f49bf7d 100644 --- a/app/assets/javascripts/reports/components/test_issue_body.vue +++ b/app/assets/javascripts/reports/components/test_issue_body.vue @@ -30,7 +30,7 @@ export default { <button type="button" class="btn-link btn-blank text-left break-link vulnerability-name-button" - @click="openModal({ issue });" + @click="openModal({ issue })" > <div v-if="isNew" class="badge badge-danger append-right-5">{{ s__('New') }}</div> {{ issue.name }} diff --git a/app/assets/javascripts/serverless/components/function_details.vue b/app/assets/javascripts/serverless/components/function_details.vue new file mode 100644 index 00000000000..2b1c21f041b --- /dev/null +++ b/app/assets/javascripts/serverless/components/function_details.vue @@ -0,0 +1,73 @@ +<script> +import PodBox from './pod_box.vue'; +import ClipboardButton from '../../vue_shared/components/clipboard_button.vue'; +import Icon from '~/vue_shared/components/icon.vue'; + +export default { + components: { + Icon, + PodBox, + ClipboardButton, + }, + props: { + func: { + type: Object, + required: true, + }, + }, + computed: { + name() { + return this.func.name; + }, + description() { + return this.func.description; + }, + funcUrl() { + return this.func.url; + }, + podCount() { + return this.func.podcount || 0; + }, + }, +}; +</script> + +<template> + <section id="serverless-function-details"> + <h3>{{ name }}</h3> + <div class="append-bottom-default"> + <div v-for="line in description.split('\n')" :key="line">{{ line }}<br /></div> + </div> + <div class="clipboard-group append-bottom-default"> + <div class="label label-monospace">{{ funcUrl }}</div> + <clipboard-button + :text="String(funcUrl)" + :title="s__('ServerlessDetails|Copy URL to clipboard')" + class="input-group-text js-clipboard-btn" + /> + <a + :href="funcUrl" + target="_blank" + rel="noopener noreferrer nofollow" + class="input-group-text btn btn-default" + > + <icon name="external-link" /> + </a> + </div> + + <h4>{{ s__('ServerlessDetails|Kubernetes Pods') }}</h4> + <div v-if="podCount > 0"> + <p> + <b v-if="podCount == 1">{{ podCount }} {{ s__('ServerlessDetails|pod in use') }}</b> + <b v-else>{{ podCount }} {{ s__('ServerlessDetails|pods in use') }}</b> + </p> + <pod-box :count="podCount" /> + <p> + {{ + s__('ServerlessDetails|Number of Kubernetes pods in use over time based on necessity.') + }} + </p> + </div> + <div v-else><p>No pods loaded at this time.</p></div> + </section> +</template> diff --git a/app/assets/javascripts/serverless/components/function_row.vue b/app/assets/javascripts/serverless/components/function_row.vue index 31f5427c771..44bfae388cb 100644 --- a/app/assets/javascripts/serverless/components/function_row.vue +++ b/app/assets/javascripts/serverless/components/function_row.vue @@ -15,8 +15,14 @@ export default { name() { return this.func.name; }, - url() { - return this.func.url; + description() { + return this.func.description; + }, + detailUrl() { + return this.func.detail_url; + }, + environment() { + return this.func.environment_scope; }, image() { return this.func.image; @@ -30,11 +36,20 @@ export default { <template> <div class="gl-responsive-table-row"> - <div class="table-section section-20">{{ name }}</div> - <div class="table-section section-50"> - <a :href="url">{{ url }}</a> + <div class="table-section section-20 section-wrap"> + <a :href="detailUrl">{{ name }}</a> + </div> + <div class="table-section section-10">{{ environment }}</div> + <div class="table-section section-40 section-wrap"> + <span class="line-break">{{ description }}</span> </div> <div class="table-section section-20">{{ image }}</div> <div class="table-section section-10"><timeago :time="timestamp" /></div> </div> </template> + +<style> +.line-break { + white-space: pre; +} +</style> diff --git a/app/assets/javascripts/serverless/components/functions.vue b/app/assets/javascripts/serverless/components/functions.vue index 349e14670b1..9606a78410e 100644 --- a/app/assets/javascripts/serverless/components/functions.vue +++ b/app/assets/javascripts/serverless/components/functions.vue @@ -50,8 +50,11 @@ export default { <div class="table-section section-20" role="rowheader"> {{ s__('Serverless|Function') }} </div> - <div class="table-section section-50" role="rowheader"> - {{ s__('Serverless|Domain') }} + <div class="table-section section-10" role="rowheader"> + {{ s__('Serverless|Cluster Env') }} + </div> + <div class="table-section section-40" role="rowheader"> + {{ s__('Serverless|Description') }} </div> <div class="table-section section-20" role="rowheader"> {{ s__('Serverless|Runtime') }} diff --git a/app/assets/javascripts/serverless/components/pod_box.vue b/app/assets/javascripts/serverless/components/pod_box.vue new file mode 100644 index 00000000000..04d3641bce3 --- /dev/null +++ b/app/assets/javascripts/serverless/components/pod_box.vue @@ -0,0 +1,36 @@ +<script> +export default { + props: { + count: { + type: Number, + required: true, + }, + color: { + type: String, + required: false, + default: 'green', + }, + }, + methods: { + boxOffset(i) { + return 20 * (i - 1); + }, + }, +}; +</script> + +<template> + <svg :width="boxOffset(count + 1)" :height="20"> + <rect + v-for="i in count" + :key="i" + width="15" + height="15" + rx="5" + ry="5" + :fill="color" + :x="boxOffset(i)" + y="0" + /> + </svg> +</template> diff --git a/app/assets/javascripts/serverless/serverless_bundle.js b/app/assets/javascripts/serverless/serverless_bundle.js index 3e3b81ba247..47a510d5fb5 100644 --- a/app/assets/javascripts/serverless/serverless_bundle.js +++ b/app/assets/javascripts/serverless/serverless_bundle.js @@ -4,23 +4,65 @@ import { s__ } from '../locale'; import Flash from '../flash'; import Poll from '../lib/utils/poll'; import ServerlessStore from './stores/serverless_store'; +import ServerlessDetailsStore from './stores/serverless_details_store'; import GetFunctionsService from './services/get_functions_service'; import Functions from './components/functions.vue'; +import FunctionDetails from './components/function_details.vue'; export default class Serverless { constructor() { - const { statusPath, clustersPath, helpPath, installed } = document.querySelector( - '.js-serverless-functions-page', - ).dataset; + if (document.querySelector('.js-serverless-function-details-page') != null) { + const { + serviceName, + serviceDescription, + serviceEnvironment, + serviceUrl, + serviceNamespace, + servicePodcount, + } = document.querySelector('.js-serverless-function-details-page').dataset; + const el = document.querySelector('#js-serverless-function-details'); + this.store = new ServerlessDetailsStore(); + const { store } = this; - this.service = new GetFunctionsService(statusPath); - this.knativeInstalled = installed !== undefined; - this.store = new ServerlessStore(this.knativeInstalled, clustersPath, helpPath); - this.initServerless(); - this.functionLoadCount = 0; + const service = { + name: serviceName, + description: serviceDescription, + environment: serviceEnvironment, + url: serviceUrl, + namespace: serviceNamespace, + podcount: servicePodcount, + }; - if (statusPath && this.knativeInstalled) { - this.initPolling(); + this.store.updateDetailedFunction(service); + this.functionDetails = new Vue({ + el, + data() { + return { + state: store.state, + }; + }, + render(createElement) { + return createElement(FunctionDetails, { + props: { + func: this.state.functionDetail, + }, + }); + }, + }); + } else { + const { statusPath, clustersPath, helpPath, installed } = document.querySelector( + '.js-serverless-functions-page', + ).dataset; + + this.service = new GetFunctionsService(statusPath); + this.knativeInstalled = installed !== undefined; + this.store = new ServerlessStore(this.knativeInstalled, clustersPath, helpPath); + this.initServerless(); + this.functionLoadCount = 0; + + if (statusPath && this.knativeInstalled) { + this.initPolling(); + } } } @@ -55,7 +97,7 @@ export default class Serverless { resource: this.service, method: 'fetchData', successCallback: data => this.handleSuccess(data), - errorCallback: () => this.handleError(), + errorCallback: () => Serverless.handleError(), }); if (!Visibility.hidden()) { @@ -64,7 +106,7 @@ export default class Serverless { this.service .fetchData() .then(data => this.handleSuccess(data)) - .catch(() => this.handleError()); + .catch(() => Serverless.handleError()); } Visibility.change(() => { @@ -102,5 +144,6 @@ export default class Serverless { } this.functions.$destroy(); + this.functionDetails.$destroy(); } } diff --git a/app/assets/javascripts/serverless/stores/serverless_details_store.js b/app/assets/javascripts/serverless/stores/serverless_details_store.js new file mode 100644 index 00000000000..5394d2cded1 --- /dev/null +++ b/app/assets/javascripts/serverless/stores/serverless_details_store.js @@ -0,0 +1,11 @@ +export default class ServerlessDetailsStore { + constructor() { + this.state = { + functionDetail: {}, + }; + } + + updateDetailedFunction(func) { + this.state.functionDetail = func; + } +} 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 f04f7606976..7f86741ed29 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 @@ -219,7 +219,7 @@ export default { name="button" type="button" class="js-clear-user-status-button clear-user-status btn" - @click="clearStatusInputs();" + @click="clearStatusInputs()" > <icon name="close" /> </button> diff --git a/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue b/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue index d3a4f9c81e0..c03b2a68c78 100644 --- a/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue +++ b/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue @@ -102,13 +102,13 @@ export default { /> <div class="title hide-collapsed"> {{ __('Time tracking') }} - <div v-if="!showHelpState" class="help-button float-right" @click="toggleHelpState(true);"> + <div v-if="!showHelpState" class="help-button float-right" @click="toggleHelpState(true)"> <i class="fa fa-question-circle" aria-hidden="true"> </i> </div> <div v-if="showHelpState" class="close-help-button float-right" - @click="toggleHelpState(false);" + @click="toggleHelpState(false)" > <i class="fa fa-close" aria-hidden="true"> </i> </div> diff --git a/app/assets/javascripts/terminal/terminal.js b/app/assets/javascripts/terminal/terminal.js index 560f50ebf8f..e5dd7a465ea 100644 --- a/app/assets/javascripts/terminal/terminal.js +++ b/app/assets/javascripts/terminal/terminal.js @@ -2,11 +2,13 @@ import _ from 'underscore'; import $ from 'jquery'; import { Terminal } from 'xterm'; import * as fit from 'xterm/lib/addons/fit/fit'; +import * as webLinks from 'xterm/lib/addons/webLinks/webLinks'; import { canScrollUp, canScrollDown } from '~/lib/utils/dom_utils'; const SCROLL_MARGIN = 5; Terminal.applyAddon(fit); +Terminal.applyAddon(webLinks); export default class GLTerminal { constructor(element, options = {}) { @@ -48,6 +50,7 @@ export default class GLTerminal { this.terminal.open(this.container); this.terminal.fit(); + this.terminal.webLinksInit(); this.terminal.focus(); this.socket.onopen = () => { diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue index 84c8a3464a5..0cafa73362e 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue @@ -12,7 +12,7 @@ export default { name: 'ReadyToMerge', components: { statusIcon, - 'squash-before-merge': SquashBeforeMerge, + SquashBeforeMerge, }, props: { mr: { type: Object, required: true }, @@ -28,6 +28,7 @@ export default { isMakingRequest: false, isMergingImmediately: false, commitMessage: this.mr.commitMessage, + squashBeforeMerge: this.mr.squash, successSvg, warningSvg, }; @@ -110,12 +111,6 @@ export default { return enableSquashBeforeMerge && commitsCount > 1; }, }, - created() { - eventHub.$on('MRWidgetUpdateSquash', this.handleUpdateSquash); - }, - beforeDestroy() { - eventHub.$off('MRWidgetUpdateSquash', this.handleUpdateSquash); - }, methods: { shouldShowMergeControls() { return this.mr.isMergeAllowed || this.shouldShowMergeWhenPipelineSucceedsText; @@ -143,7 +138,7 @@ export default { commit_message: this.commitMessage, merge_when_pipeline_succeeds: this.setToMergeWhenPipelineSucceeds, should_remove_source_branch: this.removeSourceBranch === true, - squash: this.mr.squash, + squash: this.squashBeforeMerge, }; this.isMakingRequest = true; @@ -166,9 +161,6 @@ export default { new Flash('Something went wrong. Please try again.'); // eslint-disable-line }); }, - handleUpdateSquash(val) { - this.mr.squash = val; - }, initiateMergePolling() { simplePoll((continuePolling, stopPolling) => { this.handleMergePolling(continuePolling, stopPolling); @@ -249,7 +241,7 @@ export default { :class="mergeButtonClass" type="button" class="qa-merge-button" - @click="handleMergeButtonClick();" + @click="handleMergeButtonClick()" > <i v-if="isMakingRequest" class="fa fa-spinner fa-spin" aria-hidden="true"></i> {{ mergeButtonText }} @@ -273,7 +265,7 @@ export default { <a class="merge_when_pipeline_succeeds qa-merge-when-pipeline-succeeds-option" href="#" - @click.prevent="handleMergeButtonClick(true);" + @click.prevent="handleMergeButtonClick(true)" > <span class="media"> <span class="merge-opt-icon" aria-hidden="true" v-html="successSvg"></span> @@ -285,7 +277,7 @@ export default { <a class="accept-merge-request qa-merge-immediately-option" href="#" - @click.prevent="handleMergeButtonClick(false, true);" + @click.prevent="handleMergeButtonClick(false, true)" > <span class="media"> <span class="merge-opt-icon" aria-hidden="true" v-html="warningSvg"></span> @@ -311,8 +303,9 @@ export default { <!-- Placeholder for EE extension of this component --> <squash-before-merge v-if="shouldShowSquashBeforeMerge" - :mr="mr" - :is-merge-button-disabled="isMergeButtonDisabled" + v-model="squashBeforeMerge" + :help-path="mr.squashBeforeMergeHelpPath" + :is-disabled="isMergeButtonDisabled" /> <span v-if="mr.ffOnlyEnabled" class="js-fast-forward-message"> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/squash_before_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/squash_before_merge.vue index e71acf0d7dd..b1f5655a15a 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/squash_before_merge.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/squash_before_merge.vue @@ -1,6 +1,5 @@ <script> import Icon from '~/vue_shared/components/icon.vue'; -import eventHub from '~/vue_merge_request_widget/event_hub'; import tooltip from '~/vue_shared/directives/tooltip'; export default { @@ -11,23 +10,19 @@ export default { tooltip, }, props: { - mr: { - type: Object, - required: true, - }, - isMergeButtonDisabled: { + value: { type: Boolean, required: true, }, - }, - data() { - return { - squashBeforeMerge: this.mr.squash, - }; - }, - methods: { - updateSquashModel() { - eventHub.$emit('MRWidgetUpdateSquash', this.squashBeforeMerge); + helpPath: { + type: String, + required: false, + default: '', + }, + isDisabled: { + type: Boolean, + required: false, + default: false, }, }, }; @@ -37,18 +32,19 @@ export default { <div class="accept-control inline"> <label class="merge-param-checkbox"> <input - v-model="squashBeforeMerge" - :disabled="isMergeButtonDisabled" + :checked="value" + :disabled="isDisabled" type="checkbox" name="squash" class="qa-squash-checkbox" - @change="updateSquashModel" + @change="$emit('input', $event.target.checked)" /> {{ __('Squash commits') }} </label> <a + v-if="helpPath" v-tooltip - :href="mr.squashBeforeMergeHelpPath" + :href="helpPath" data-title="About this feature" data-placement="bottom" target="_blank" diff --git a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue index b7f12076958..5a9d86594b1 100644 --- a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue +++ b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue @@ -32,7 +32,6 @@ import MRWidgetStore from './stores/ee_switch_mr_widget_store'; import MRWidgetService from './services/ee_switch_mr_widget_service'; import eventHub from './event_hub'; import stateMaps from './stores/ee_switch_state_maps'; -import SquashBeforeMerge from './components/states/squash_before_merge.vue'; import notify from '~/lib/utils/notify'; import SourceBranchRemovalStatus from './components/source_branch_removal_status.vue'; import GroupedTestReportsApp from '../reports/components/grouped_test_reports_app.vue'; @@ -59,7 +58,6 @@ export default { 'mr-widget-missing-branch': MissingBranchState, 'mr-widget-ready-to-merge': ReadyToMergeState, 'sha-mismatch': ShaMismatchState, - 'mr-widget-squash-before-merge': SquashBeforeMerge, 'mr-widget-checking': CheckingState, 'mr-widget-unresolved-discussions': UnresolvedDiscussionsState, 'mr-widget-pipeline-blocked': PipelineBlockedState, diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js b/app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js index 066a3b833d7..0cc4fd59f5e 100644 --- a/app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js +++ b/app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js @@ -13,6 +13,8 @@ export default function deviseState(data) { return stateKey.conflicts; } else if (data.work_in_progress) { return stateKey.workInProgress; + } else if (this.shouldBeRebased) { + return stateKey.rebase; } else if (this.onlyAllowMergeIfPipelineSucceeds && this.isPipelineFailed) { return stateKey.pipelineFailed; } else if (this.hasMergeableDiscussionsState) { @@ -25,8 +27,6 @@ export default function deviseState(data) { return this.mergeError ? stateKey.autoMergeFailed : stateKey.mergeWhenPipelineSucceeds; } else if (!this.canMerge) { return stateKey.notAllowedToMerge; - } else if (this.shouldBeRebased) { - return stateKey.rebase; } else if (this.canBeMerged) { return stateKey.readyToMerge; } diff --git a/app/assets/javascripts/vue_shared/components/bar_chart.vue b/app/assets/javascripts/vue_shared/components/bar_chart.vue index 4abf795f7bd..eabf5d4bf60 100644 --- a/app/assets/javascripts/vue_shared/components/bar_chart.vue +++ b/app/assets/javascripts/vue_shared/components/bar_chart.vue @@ -293,8 +293,8 @@ export default { :title="setTooltipTitle(data)" class="bar-rect" data-placement="top" - @mouseover="barHoveredIn(index);" - @mouseout="barHoveredOut(index);" + @mouseover="barHoveredIn(index)" + @mouseout="barHoveredOut(index)" /> </template> </g> diff --git a/app/assets/javascripts/vue_shared/components/deprecated_modal.vue b/app/assets/javascripts/vue_shared/components/deprecated_modal.vue index 2129f90d497..36b3ee05456 100644 --- a/app/assets/javascripts/vue_shared/components/deprecated_modal.vue +++ b/app/assets/javascripts/vue_shared/components/deprecated_modal.vue @@ -95,7 +95,7 @@ export default { class="close float-right" data-dismiss="modal" aria-label="Close" - @click="emitCancel($event);" + @click="emitCancel($event)" > <span aria-hidden="true">×</span> </button> @@ -112,7 +112,7 @@ export default { type="button" class="btn" data-dismiss="modal" - @click="emitCancel($event);" + @click="emitCancel($event)" > {{ closeButtonLabel }} </button> @@ -130,7 +130,7 @@ export default { type="button" class="btn js-primary-button" data-dismiss="modal" - @click="emitSubmit($event);" + @click="emitSubmit($event)" > {{ primaryButtonLabel }} </button> diff --git a/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff_viewer.vue b/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff_viewer.vue index d5fda7e4ed3..cab92297ca7 100644 --- a/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff_viewer.vue +++ b/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff_viewer.vue @@ -75,7 +75,7 @@ export default { :class="{ active: mode === $options.imageViewMode.twoup, }" - @click="changeMode($options.imageViewMode.twoup);" + @click="changeMode($options.imageViewMode.twoup)" > {{ s__('ImageDiffViewer|2-up') }} </li> @@ -83,7 +83,7 @@ export default { :class="{ active: mode === $options.imageViewMode.swipe, }" - @click="changeMode($options.imageViewMode.swipe);" + @click="changeMode($options.imageViewMode.swipe)" > {{ s__('ImageDiffViewer|Swipe') }} </li> @@ -91,7 +91,7 @@ export default { :class="{ active: mode === $options.imageViewMode.onion, }" - @click="changeMode($options.imageViewMode.onion);" + @click="changeMode($options.imageViewMode.onion)" > {{ s__('ImageDiffViewer|Onion skin') }} </li> diff --git a/app/assets/javascripts/vue_shared/components/file_row.vue b/app/assets/javascripts/vue_shared/components/file_row.vue index 4c884c55a30..f54033efc54 100644 --- a/app/assets/javascripts/vue_shared/components/file_row.vue +++ b/app/assets/javascripts/vue_shared/components/file_row.vue @@ -139,8 +139,8 @@ export default { class="file-row" role="button" @click="clickFile" - @mouseover="toggleHover(true);" - @mouseout="toggleHover(false);" + @mouseover="toggleHover(true)" + @mouseout="toggleHover(false)" > <div class="file-row-name-container"> <span ref="textOutput" :style="levelIndentation" class="file-row-name str-truncated"> diff --git a/app/assets/javascripts/vue_shared/components/gl_modal.vue b/app/assets/javascripts/vue_shared/components/gl_modal.vue index faf4181bbaf..438851e5ac7 100644 --- a/app/assets/javascripts/vue_shared/components/gl_modal.vue +++ b/app/assets/javascripts/vue_shared/components/gl_modal.vue @@ -81,7 +81,7 @@ export default { type="button" class="close js-modal-close-action" data-dismiss="modal" - @click="emitCancel($event);" + @click="emitCancel($event)" > <span aria-hidden="true">×</span> </button> @@ -96,7 +96,7 @@ export default { type="button" class="btn js-modal-cancel-action qa-modal-cancel-button" data-dismiss="modal" - @click="emitCancel($event);" + @click="emitCancel($event)" > {{ s__('Modal|Cancel') }} </button> @@ -105,7 +105,7 @@ export default { type="button" class="btn js-modal-primary-action qa-modal-primary-button" data-dismiss="modal" - @click="emitSubmit($event);" + @click="emitSubmit($event)" > {{ footerPrimaryButtonText }} </button> diff --git a/app/assets/javascripts/vue_shared/components/header_ci_component.vue b/app/assets/javascripts/vue_shared/components/header_ci_component.vue index c830f5b49b6..3f45dc7853b 100644 --- a/app/assets/javascripts/vue_shared/components/header_ci_component.vue +++ b/app/assets/javascripts/vue_shared/components/header_ci_component.vue @@ -148,7 +148,7 @@ export default { :class="action.cssClass" container-class="d-inline" :label="action.label" - @click="onClickAction(action);" + @click="onClickAction(action)" /> </template> </section> diff --git a/app/assets/javascripts/vue_shared/components/markdown/field.vue b/app/assets/javascripts/vue_shared/components/markdown/field.vue index 937a2847a58..cc07ef46064 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/field.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/field.vue @@ -182,9 +182,9 @@ export default { this.hasSuggestion = data.references.suggestions && data.references.suggestions.length; } - this.$nextTick(() => { - $(this.$refs['markdown-preview']).renderGFM(); - }); + this.$nextTick() + .then(() => $(this.$refs['markdown-preview']).renderGFM()) + .catch(() => new Flash(__('Error rendering markdown preview'))); }, versionedPreviewPath() { diff --git a/app/assets/javascripts/vue_shared/components/markdown/header.vue b/app/assets/javascripts/vue_shared/components/markdown/header.vue index bf4d42670ee..dbfa32cd0ce 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/header.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/header.vue @@ -78,12 +78,7 @@ export default { <div class="md-header"> <ul class="nav-links clearfix"> <li :class="{ active: !previewMarkdown }" class="md-header-tab"> - <button - class="js-write-link" - tabindex="-1" - type="button" - @click="writeMarkdownTab($event);" - > + <button class="js-write-link" tabindex="-1" type="button" @click="writeMarkdownTab($event)"> Write </button> </li> @@ -92,7 +87,7 @@ export default { class="js-preview-link js-md-preview-button" tabindex="-1" type="button" - @click="previewMarkdownTab($event);" + @click="previewMarkdownTab($event)" > Preview </button> diff --git a/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff.vue b/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff.vue index f98560f7336..b9f884074d0 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff.vue @@ -50,7 +50,7 @@ export default { :help-page-path="helpPagePath" @apply="applySuggestion" /> - <table class="mb-3 md-suggestion-diff"> + <table class="mb-3 md-suggestion-diff js-syntax-highlight code"> <tbody> <!-- Old Line --> <tr class="line_holder old"> 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 563e2f94fcc..c5a2aa1f2af 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 @@ -42,7 +42,7 @@ export default { <div class="md-suggestion-header border-bottom-0 mt-2"> <div class="qa-suggestion-diff-header font-weight-bold"> {{ __('Suggested change') }} - <a v-if="helpPagePath" :href="helpPagePath" :aria-label="__('Help')"> + <a v-if="helpPagePath" :href="helpPagePath" :aria-label="__('Help')" class="js-help-btn"> <icon name="question-o" css-classes="link-highlight" /> </a> </div> diff --git a/app/assets/javascripts/vue_shared/components/navigation_tabs.vue b/app/assets/javascripts/vue_shared/components/navigation_tabs.vue index 09a64502819..f8983a3d29a 100644 --- a/app/assets/javascripts/vue_shared/components/navigation_tabs.vue +++ b/app/assets/javascripts/vue_shared/components/navigation_tabs.vue @@ -58,7 +58,7 @@ export default { active: tab.isActive, }" > - <a :class="`js-${scope}-tab-${tab.scope}`" role="button" @click="onTabClick(tab);"> + <a :class="`js-${scope}-tab-${tab.scope}`" role="button" @click="onTabClick(tab)"> {{ tab.name }} <span v-if="shouldRenderBadge(tab.count)" class="badge badge-pill"> {{ tab.count }} </span> diff --git a/app/assets/javascripts/vue_shared/components/notes/system_note.vue b/app/assets/javascripts/vue_shared/components/notes/system_note.vue index 31df26f7b05..b0af8399955 100644 --- a/app/assets/javascripts/vue_shared/components/notes/system_note.vue +++ b/app/assets/javascripts/vue_shared/components/notes/system_note.vue @@ -97,7 +97,7 @@ export default { v-html="note.note_html" ></div> <div v-if="hasMoreCommits" class="flex-list"> - <div class="system-note-commit-list-toggler flex-row" @click="expanded = !expanded;"> + <div class="system-note-commit-list-toggler flex-row" @click="expanded = !expanded"> <icon :name="toggleIcon" :size="8" class="append-right-5" /> <span>Toggle commit list</span> </div> diff --git a/app/assets/javascripts/vue_shared/components/recaptcha_modal.vue b/app/assets/javascripts/vue_shared/components/recaptcha_modal.vue index 1c6c3fc4734..df19906309c 100644 --- a/app/assets/javascripts/vue_shared/components/recaptcha_modal.vue +++ b/app/assets/javascripts/vue_shared/components/recaptcha_modal.vue @@ -19,7 +19,7 @@ export default { data() { return { script: {}, - scriptSrc: 'https://www.google.com/recaptcha/api.js', + scriptSrc: 'https://www.recaptcha.net/recaptcha/api.js', }; }, diff --git a/app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue b/app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue index 82067129c57..6c0c7f15943 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue @@ -134,7 +134,7 @@ export default { <button type="button" class="btn-blank btn-link btn-secondary-hover-link" - @click="newDateSelected(null);" + @click="newDateSelected(null)" > remove </button> diff --git a/app/assets/javascripts/vue_shared/components/table_pagination.vue b/app/assets/javascripts/vue_shared/components/table_pagination.vue index 01e655d27e5..2a34b4630f2 100644 --- a/app/assets/javascripts/vue_shared/components/table_pagination.vue +++ b/app/assets/javascripts/vue_shared/components/table_pagination.vue @@ -149,7 +149,7 @@ export default { }" class="page-item" > - <a class="page-link" @click.prevent="changePage(item.title, item.disabled);"> + <a class="page-link" @click.prevent="changePage(item.title, item.disabled)"> {{ item.title }} </a> </li> diff --git a/app/assets/stylesheets/bootstrap_migration.scss b/app/assets/stylesheets/bootstrap_migration.scss index 587127bb059..c8357f7751c 100644 --- a/app/assets/stylesheets/bootstrap_migration.scss +++ b/app/assets/stylesheets/bootstrap_migration.scss @@ -225,7 +225,7 @@ h3.popover-header { } .info-well { - background: $theme-gray-50; + background: $gray-50; color: $gl-text-color; border: 1px solid $border-color; border-radius: 4px; diff --git a/app/assets/stylesheets/framework/animations.scss b/app/assets/stylesheets/framework/animations.scss index 43d4044033f..4fb787887a1 100644 --- a/app/assets/stylesheets/framework/animations.scss +++ b/app/assets/stylesheets/framework/animations.scss @@ -204,7 +204,7 @@ a { [class^="skeleton-line-"] { position: relative; - background-color: $theme-gray-100; + background-color: $gray-100; height: 10px; overflow: hidden; @@ -220,10 +220,10 @@ a { background-size: cover; background-image: linear-gradient( to right, - $theme-gray-100 0%, - $theme-gray-50 20%, - $theme-gray-100 40%, - $theme-gray-100 100% + $gray-100 0%, + $gray-50 20%, + $gray-100 40%, + $gray-100 100% ); height: 10px; } diff --git a/app/assets/stylesheets/framework/awards.scss b/app/assets/stylesheets/framework/awards.scss index 7a95db5976d..ad650d45314 100644 --- a/app/assets/stylesheets/framework/awards.scss +++ b/app/assets/stylesheets/framework/awards.scss @@ -176,7 +176,7 @@ &.user-authored { cursor: default; background-color: $gray-light; - border-color: $theme-gray-200; + border-color: $gray-200; color: $gl-text-color-disabled; gl-emoji { diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss index a4a9276c580..5d2cbdde8dc 100644 --- a/app/assets/stylesheets/framework/buttons.scss +++ b/app/assets/stylesheets/framework/buttons.scss @@ -483,7 +483,7 @@ // All disabled buttons, regardless of color, type, etc %disabled { background-color: $gray-light !important; - border-color: $theme-gray-200 !important; + border-color: $gray-200 !important; color: $gl-text-color-disabled !important; opacity: 1 !important; cursor: default !important; diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index afcb230797a..b90db135b4a 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -32,6 +32,10 @@ max-height: $dropdown-max-height; overflow-y: auto; + &.dropdown-extended-height { + max-height: 400px; + } + @include media-breakpoint-down(xs) { width: 100%; } @@ -125,7 +129,7 @@ @extend .dropdown-toggle; padding-right: 25px; position: relative; - width: 163px; + width: 160px; text-overflow: ellipsis; overflow: hidden; diff --git a/app/assets/stylesheets/framework/forms.scss b/app/assets/stylesheets/framework/forms.scss index 0a0ef2071e9..cbf9ee24ec5 100644 --- a/app/assets/stylesheets/framework/forms.scss +++ b/app/assets/stylesheets/framework/forms.scss @@ -261,7 +261,7 @@ label { right: 0.8em; top: 50%; transform: translateY(-50%); - color: $theme-gray-600; + color: $gray-600; } } diff --git a/app/assets/stylesheets/framework/gitlab_theme.scss b/app/assets/stylesheets/framework/gitlab_theme.scss index 0ef50e139f2..418eafa153c 100644 --- a/app/assets/stylesheets/framework/gitlab_theme.scss +++ b/app/assets/stylesheets/framework/gitlab_theme.scss @@ -282,31 +282,31 @@ body { &.ui-dark { @include gitlab-theme( - $theme-gray-200, - $theme-gray-500, - $theme-gray-700, - $theme-gray-800, - $theme-gray-900, + $gray-200, + $gray-500, + $gray-700, + $gray-800, + $gray-900, $white-light ); } &.ui-light { @include gitlab-theme( - $theme-gray-700, - $theme-gray-800, - $theme-gray-700, - $theme-gray-700, - $theme-gray-100, - $theme-gray-700 + $gray-700, + $gray-800, + $gray-700, + $gray-700, + $gray-100, + $gray-700 ); .navbar-gitlab { - background-color: $theme-gray-100; + background-color: $gray-100; box-shadow: 0 1px 0 0 $border-color; .logo-text svg { - fill: $theme-gray-900; + fill: $gray-900; } .navbar-sub-nav, @@ -315,7 +315,7 @@ body { > a:hover, > a:focus, > button:hover { - color: $theme-gray-900; + color: $gray-900; } &.active > a, @@ -329,8 +329,8 @@ body { .container-fluid { .navbar-toggler, .navbar-toggler:hover { - color: $theme-gray-700; - border-left: 1px solid $theme-gray-200; + color: $gray-700; + border-left: 1px solid $gray-200; } } } @@ -348,7 +348,7 @@ body { .search-input-wrap { .search-icon { - fill: $theme-gray-200; + fill: $gray-200; } .search-input { @@ -359,16 +359,16 @@ body { .nav-sidebar li.active { > a { - color: $theme-gray-900; + color: $gray-900; } svg { - fill: $theme-gray-900; + fill: $gray-900; } } .sidebar-top-level-items > li.active .badge.badge-pill { - color: $theme-gray-900; + color: $gray-900; } } } diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss index 5574873fa22..36dd1cee4de 100644 --- a/app/assets/stylesheets/framework/header.scss +++ b/app/assets/stylesheets/framework/header.scss @@ -596,6 +596,10 @@ .emoji-menu-toggle-button { @include emoji-menu-toggle-button; } + + .input-group { + height: 34px; + } } .nav-links > li > a { diff --git a/app/assets/stylesheets/framework/icons.scss b/app/assets/stylesheets/framework/icons.scss index 8db7d63266e..49b9b7014ae 100644 --- a/app/assets/stylesheets/framework/icons.scss +++ b/app/assets/stylesheets/framework/icons.scss @@ -87,8 +87,8 @@ display: flex; align-items: center; justify-content: center; - border: $border-size solid $theme-gray-400; + border: $border-size solid $gray-400; border-radius: 50%; padding: $gl-padding-8 - $border-size; - color: $theme-gray-700; + color: $gray-700; } diff --git a/app/assets/stylesheets/framework/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss index ce46d760d7b..679148ddf7b 100644 --- a/app/assets/stylesheets/framework/markdown_area.scss +++ b/app/assets/stylesheets/framework/markdown_area.scss @@ -158,7 +158,7 @@ max-height: calc(100vh - 100px); } - table { + table:not(.js-syntax-highlight) { @include markdown-table; } } diff --git a/app/assets/stylesheets/framework/stacked_progress_bar.scss b/app/assets/stylesheets/framework/stacked_progress_bar.scss index 29a2d5881f7..2255d3be75b 100644 --- a/app/assets/stylesheets/framework/stacked_progress_bar.scss +++ b/app/assets/stylesheets/framework/stacked_progress_bar.scss @@ -3,7 +3,7 @@ height: 16px; border-radius: 10px; overflow: hidden; - background-color: $theme-gray-100; + background-color: $gray-100; .status-unavailable, .status-green, @@ -24,7 +24,7 @@ .status-unavailable { padding: 0 10px; - color: $theme-gray-700; + color: $gray-700; } .status-green { @@ -36,11 +36,11 @@ } .status-neutral { - background-color: $theme-gray-200; + background-color: $gray-200; color: $gl-gray-dark; &:hover { - background-color: $theme-gray-300; + background-color: $gray-300; } } diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss index 0c81dc2e156..45dab036d35 100644 --- a/app/assets/stylesheets/framework/typography.scss +++ b/app/assets/stylesheets/framework/typography.scss @@ -143,7 +143,7 @@ margin: 0 0 16px; } - table { + table:not(.js-syntax-highlight) { @extend .table; @extend .table-bordered; margin: 16px 0; diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 242977e8543..c1666c728f3 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -97,6 +97,18 @@ $red-800: #8b2615; $red-900: #711e11; $red-950: #4b140b; +$gray-50: #fafafa; +$gray-100: #f2f2f2; +$gray-200: #dfdfdf; +$gray-300: #cccccc; +$gray-400: #bababa; +$gray-500: #a7a7a7; +$gray-600: #919191; +$gray-700: #707070; +$gray-800: #4f4f4f; +$gray-900: #2e2e2e; +$gray-950: #1f1f1f; + // GitLab themes $indigo-50: #f7f7ff; @@ -111,18 +123,6 @@ $indigo-800: #393982; $indigo-900: #292961; $indigo-950: #1a1a40; -$theme-gray-50: #fafafa; -$theme-gray-100: #f2f2f2; -$theme-gray-200: #dfdfdf; -$theme-gray-300: #cccccc; -$theme-gray-400: #bababa; -$theme-gray-500: #a7a7a7; -$theme-gray-600: #919191; -$theme-gray-700: #707070; -$theme-gray-800: #4f4f4f; -$theme-gray-900: #2e2e2e; -$theme-gray-950: #1f1f1f; - $theme-blue-50: #f4f8fc; $theme-blue-100: #e6edf5; $theme-blue-200: #c8d7e6; diff --git a/app/assets/stylesheets/framework/variables_overrides.scss b/app/assets/stylesheets/framework/variables_overrides.scss index 069f45bff49..d5f8e3fb4ee 100644 --- a/app/assets/stylesheets/framework/variables_overrides.scss +++ b/app/assets/stylesheets/framework/variables_overrides.scss @@ -5,7 +5,7 @@ $secondary: $gray-light; $input-disabled-bg: $gray-light; -$input-border-color: $theme-gray-200; +$input-border-color: $gray-200; $input-color: $gl-text-color; $font-family-sans-serif: $regular-font; $font-family-monospace: $monospace-font; @@ -20,7 +20,7 @@ $warning: $orange-500; $danger: $red-500; $zindex-modal-backdrop: 1040; $nav-divider-margin-y: ($grid-size / 2); -$dropdown-divider-bg: $theme-gray-200; +$dropdown-divider-bg: $gray-200; $dropdown-item-padding-y: 8px; $dropdown-item-padding-x: 12px; $popover-max-width: 300px; diff --git a/app/assets/stylesheets/page_bundles/ide.scss b/app/assets/stylesheets/page_bundles/ide.scss index 98d0a2d43ea..553cc44fe83 100644 --- a/app/assets/stylesheets/page_bundles/ide.scss +++ b/app/assets/stylesheets/page_bundles/ide.scss @@ -130,7 +130,7 @@ $ide-commit-header-height: 48px; background: none; border: 0; border-radius: $border-radius-default; - color: $theme-gray-900; + color: $gray-900; svg { position: relative; @@ -145,7 +145,7 @@ $ide-commit-header-height: 48px; } &:not([disabled]):hover { - background-color: $theme-gray-200; + background-color: $gray-200; } &:not([disabled]):focus { @@ -265,7 +265,7 @@ $ide-commit-header-height: 48px; .margin { background-color: $white-light; - border-right: 1px solid $theme-gray-100; + border-right: 1px solid $gray-100; .line-insert { border-right: 1px solid $line-added-dark; @@ -292,7 +292,7 @@ $ide-commit-header-height: 48px; .monaco-editor, .monaco-editor-background, .monaco-editor .inputarea.ime-input { - background-color: $theme-gray-50; + background-color: $gray-50; } } } @@ -527,7 +527,7 @@ $ide-commit-header-height: 48px; display: block; margin-left: auto; margin-right: auto; - color: $theme-gray-700; + color: $gray-700; } .file-status-icon { @@ -551,7 +551,7 @@ $ide-commit-header-height: 48px; &:hover, &:focus { - background: $theme-gray-100; + background: $gray-100; outline: 0; @@ -563,7 +563,7 @@ $ide-commit-header-height: 48px; } &:active { - background: $theme-gray-200; + background: $gray-200; } &.is-active { @@ -767,12 +767,12 @@ $ide-commit-header-height: 48px; &:hover { color: $gl-text-color; - background-color: $theme-gray-100; + background-color: $gray-100; } &:focus { color: $gl-text-color; - background-color: $theme-gray-200; + background-color: $gray-200; } &.active { @@ -1273,10 +1273,10 @@ $ide-commit-header-height: 48px; .ide-entry-dropdown-toggle { padding: $gl-padding-4; color: $gl-text-color; - background-color: $theme-gray-100; + background-color: $gray-100; &:hover { - background-color: $theme-gray-200; + background-color: $gray-200; } &:active, @@ -1331,7 +1331,7 @@ $ide-commit-header-height: 48px; &:focus { outline: 0; box-shadow: none; - border-color: $theme-gray-200; + border-color: $gray-200; } } @@ -1358,7 +1358,7 @@ $ide-commit-header-height: 48px; .ide-commit-editor-header { height: 65px; padding: 8px 16px; - background-color: $theme-gray-50; + background-color: $gray-50; box-shadow: inset 0 -1px $white-dark; } @@ -1370,7 +1370,7 @@ $ide-commit-header-height: 48px; .ide-file-icon-holder { display: flex; align-items: center; - color: $theme-gray-700; + color: $gray-700; } .file-row:hover, diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss index 37984a8666f..e1d1e598da8 100644 --- a/app/assets/stylesheets/pages/boards.scss +++ b/app/assets/stylesheets/pages/boards.scss @@ -29,10 +29,6 @@ .dropdown-menu-issues-board-new { width: 320px; - .open & { - max-height: 400px; - } - .dropdown-content { max-height: 162px; } @@ -281,7 +277,7 @@ padding: $gl-padding; background: $white-light; border-radius: $border-radius-default; - border: 1px solid $theme-gray-200; + border: 1px solid $gray-200; box-shadow: 0 1px 2px $issue-boards-card-shadow; list-style: none; line-height: $gl-padding; @@ -673,7 +669,7 @@ } .board-card-info-icon { - color: $theme-gray-600; + color: $gray-600; margin-right: $gl-padding-4; } diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss index 1352a004206..65f46e3852a 100644 --- a/app/assets/stylesheets/pages/builds.scss +++ b/app/assets/stylesheets/pages/builds.scss @@ -261,7 +261,7 @@ .trigger-variables-table-cell { font-size: $gl-font-size-small; line-height: $gl-line-height; - border: 1px solid $theme-gray-200; + border: 1px solid $gray-200; padding: $gl-padding-4 6px; width: 50%; vertical-align: top; @@ -272,7 +272,13 @@ } .retry-link { - display: none; + display: block; + + .btn { + i { + margin-left: 5px; + } + } .btn-inverted-secondary { color: $blue-500; @@ -281,16 +287,6 @@ color: $white-light; } } - - @include media-breakpoint-down(sm) { - display: block; - - .btn { - i { - margin-left: 5px; - } - } - } } .stage-item { diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss index 18c62cb4f1e..b78f11aadf1 100644 --- a/app/assets/stylesheets/pages/diff.scss +++ b/app/assets/stylesheets/pages/diff.scss @@ -9,7 +9,7 @@ @media (min-width: map-get($grid-breakpoints, md)) { position: -webkit-sticky; position: sticky; - top: 92px; + top: $header-height + 51px; margin-left: -1px; border-left: 1px solid $border-color; z-index: 102; diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss index 75166ffcada..b6abb792709 100644 --- a/app/assets/stylesheets/pages/environments.scss +++ b/app/assets/stylesheets/pages/environments.scss @@ -267,7 +267,7 @@ .prometheus-graph-cursor { position: absolute; - background: $theme-gray-600; + background: $gray-600; width: 1px; } @@ -309,7 +309,7 @@ > .arrow::after { border-top: 6px solid transparent; border-bottom: 6px solid transparent; - border-left: 4px solid $theme-gray-50; + border-left: 4px solid $gray-50; } .arrow-shadow { @@ -331,7 +331,7 @@ > .arrow::after { border-top: 6px solid transparent; border-bottom: 6px solid transparent; - border-right: 4px solid $theme-gray-50; + border-right: 4px solid $gray-50; } .arrow-shadow { @@ -364,7 +364,7 @@ } > .popover-title { - background-color: $theme-gray-50; + background-color: $gray-50; border-radius: $border-radius-default $border-radius-default 0 0; } } @@ -439,7 +439,7 @@ } > text { - fill: $theme-gray-600; + fill: $gray-600; font-size: 10px; } } @@ -482,5 +482,5 @@ } .prometheus-table-row-highlight { - background-color: $theme-gray-100; + background-color: $gray-100; } diff --git a/app/assets/stylesheets/pages/graph.scss b/app/assets/stylesheets/pages/graph.scss index 4fb1a956fab..83b1680512d 100644 --- a/app/assets/stylesheets/pages/graph.scss +++ b/app/assets/stylesheets/pages/graph.scss @@ -44,7 +44,7 @@ } .x-axis-text { - fill: $theme-gray-900; + fill: $gray-900; } .bar-rect { @@ -64,7 +64,7 @@ text { font-weight: bold; font-size: 12px; - fill: $theme-gray-800; + fill: $gray-800; } } } @@ -87,5 +87,5 @@ .animate-flicker { animation: flickerAnimation 1.5s infinite; - fill: $theme-gray-500; + fill: $gray-500; } diff --git a/app/assets/stylesheets/pages/groups.scss b/app/assets/stylesheets/pages/groups.scss index f0cb81e0bc3..ebbb5beed81 100644 --- a/app/assets/stylesheets/pages/groups.scss +++ b/app/assets/stylesheets/pages/groups.scss @@ -78,7 +78,7 @@ &:hover { background-color: $gray-darker; - color: $theme-gray-900; + color: $gray-900; } } diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index a1069aa9783..e0bdc1341b1 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -934,7 +934,7 @@ .sidebar-collapsed-divider { line-height: 5px; font-size: 12px; - color: $theme-gray-700; + color: $gray-700; + .sidebar-collapsed-icon { padding-top: 0; diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss index d2b9470be69..2372640277e 100644 --- a/app/assets/stylesheets/pages/labels.scss +++ b/app/assets/stylesheets/pages/labels.scss @@ -82,7 +82,7 @@ justify-content: space-between; padding: $gl-padding; border-radius: $border-radius-default; - border: 1px solid $theme-gray-100; + border: 1px solid $gray-100; &:last-child { margin-bottom: 0; @@ -257,7 +257,7 @@ } .label-badge { - color: $theme-gray-900; + color: $gray-900; font-weight: $gl-font-weight-normal; padding: $gl-padding-4 $gl-padding-8; border-radius: $border-radius-default; @@ -269,7 +269,7 @@ } .label-badge-gray { - background-color: $theme-gray-100; + background-color: $gray-100; } .label-links { @@ -326,11 +326,11 @@ } .label-action { - color: $theme-gray-800; + color: $gray-800; cursor: pointer; svg { - fill: $theme-gray-800; + fill: $gray-800; } &:hover { diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index 221b4e934ff..1e4b8d8b7e4 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -64,7 +64,7 @@ &::before { content: ''; - border-left: 1px solid $theme-gray-200; + border-left: 1px solid $gray-200; position: absolute; left: 32px; top: -17px; @@ -907,7 +907,7 @@ } .btn svg { - fill: $theme-gray-700; + fill: $gray-700; } .dropdown-menu { @@ -951,7 +951,7 @@ .coverage { font-size: 12px; - color: $theme-gray-700; + color: $gray-700; line-height: initial; } diff --git a/app/assets/stylesheets/pages/milestone.scss b/app/assets/stylesheets/pages/milestone.scss index 94bf32945fc..15f3a2ef4a8 100644 --- a/app/assets/stylesheets/pages/milestone.scss +++ b/app/assets/stylesheets/pages/milestone.scss @@ -8,7 +8,7 @@ $status-box-line-height: 26px; padding: $gl-padding-8; margin-top: $gl-padding-8; border-radius: $border-radius-default; - background-color: $theme-gray-100; + background-color: $gray-100; .milestone { border: 0; diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss index 5b30295adf9..51f755c67af 100644 --- a/app/assets/stylesheets/pages/note_form.scss +++ b/app/assets/stylesheets/pages/note_form.scss @@ -1,10 +1,6 @@ /** * Note Form */ -.comment-btn { - @extend .btn-success; -} - .diff-file .diff-content { tr.line_holder:hover > td .line_note_link { opacity: 1; @@ -151,7 +147,7 @@ } .sidebar-item-value & { - fill: $theme-gray-700; + fill: $gray-700; } } @@ -386,7 +382,7 @@ table { } .comment-type-dropdown { - .comment-btn { + .btn-success { width: auto; } @@ -417,7 +413,7 @@ table { width: 100%; margin-bottom: 10px; - .comment-btn { + .btn-success { flex-grow: 1; flex-shrink: 0; width: auto; diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index a5b1eff3e1d..23b9e4f9416 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -5,7 +5,7 @@ $note-form-margin-left: 72px; @mixin vertical-line($left) { &::before { content: ''; - border-left: 2px solid $theme-gray-100; + border-left: 2px solid $gray-100; position: absolute; top: 0; bottom: 0; @@ -153,12 +153,12 @@ $note-form-margin-left: 72px; position: relative; .timeline-discussion-body { - margin-top: -8px; + margin-top: -$gl-padding-8; overflow-x: auto; overflow-y: hidden; - .discussion-resolved-text { - margin-bottom: 8px; + .note-body { + margin-top: $gl-padding-8; } } diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index 7a47e0a2836..a28921592ec 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -256,14 +256,25 @@ } } - .mini-pipeline-graph-dropdown-toggle svg { - height: $ci-action-icon-size; - width: $ci-action-icon-size; - position: absolute; - top: -1px; - left: -1px; - z-index: 2; - overflow: visible; + .mini-pipeline-graph-dropdown-toggle { + svg { + height: $ci-action-icon-size; + width: $ci-action-icon-size; + position: absolute; + top: -1px; + left: -1px; + z-index: 2; + overflow: visible; + } + + &:hover, + &:active, + &:focus { + svg { + top: -2px; + left: -2px; + } + } } .stage-container { @@ -293,7 +304,7 @@ width: 7px; position: absolute; right: -7px; - top: 10px; + top: 11px; border-bottom: 2px solid $border-color; } } @@ -433,6 +444,7 @@ } .pipeline-tab-content { + display: flex; width: 100%; background-color: $gray-light; padding: $gl-padding; @@ -707,21 +719,43 @@ font-weight: $gl-font-weight-normal; } -@mixin mini-pipeline-graph-color($color-light, $color-main, $color-dark) { - border-color: $color-main; - color: $color-main; +@mixin mini-pipeline-graph-color( + $color-background-default, + $color-background-hover-focus, + $color-background-active, + $color-foreground-default, + $color-foreground-hover-focus, + $color-foreground-active +) { + background-color: $color-background-default; + border-color: $color-foreground-default; + + svg { + fill: $color-foreground-default; + } &:hover, - &:focus, + &:focus { + background-color: $color-background-hover-focus; + border-color: $color-foreground-hover-focus; + + svg { + fill: $color-foreground-hover-focus; + } + } + &:active { - background-color: $color-light; - border-color: $color-dark; - color: $color-dark; + background-color: $color-background-active; + border-color: $color-foreground-active; svg { - fill: $color-dark; + fill: $color-foreground-active; } } + + &:focus { + box-shadow: 0 0 4px 1px $blue-300; + } } @mixin mini-pipeline-item() { @@ -733,26 +767,32 @@ height: $ci-action-icon-size; margin: 0; padding: 0; - transition: all 0.2s linear; position: relative; vertical-align: middle; + &:hover, + &:active, + &:focus { + outline: none; + border-width: 2px; + } + // Dropdown button animation in mini pipeline graph &.ci-status-icon-success { - @include mini-pipeline-graph-color($green-100, $green-500, $green-600); + @include mini-pipeline-graph-color($white, $green-100, $green-200, $green-500, $green-600, $green-700); } &.ci-status-icon-failed { - @include mini-pipeline-graph-color($red-100, $red-500, $red-600); + @include mini-pipeline-graph-color($white, $red-100, $red-200, $red-500, $red-600, $red-700); } &.ci-status-icon-pending, &.ci-status-icon-success_with_warnings { - @include mini-pipeline-graph-color($orange-100, $orange-500, $orange-600); + @include mini-pipeline-graph-color($white, $orange-100, $orange-200, $orange-500, $orange-600, $orange-700); } &.ci-status-icon-running { - @include mini-pipeline-graph-color($blue-100, $blue-400, $blue-600); + @include mini-pipeline-graph-color($white, $blue-100, $blue-200, $blue-500, $blue-600, $blue-700); } &.ci-status-icon-canceled, @@ -760,42 +800,18 @@ &.ci-status-icon-disabled, &.ci-status-icon-not-found, &.ci-status-icon-manual { - @include mini-pipeline-graph-color(rgba($gl-text-color, 0.1), $gl-text-color, $gl-text-color); + @include mini-pipeline-graph-color($white, $gray-700, $gray-800, $gray-900, $gray-950, $black); } &.ci-status-icon-created, &.ci-status-icon-skipped { - @include mini-pipeline-graph-color(rgba($gray-darkest, 0.1), $gray-darkest, $gray-darkest); + @include mini-pipeline-graph-color($white, $gray-200, $gray-300, $gray-500, $gray-600, $gray-700); } } // Dropdown button in mini pipeline graph button.mini-pipeline-graph-dropdown-toggle { @include mini-pipeline-item(); - - > .fa.fa-caret-down { - position: absolute; - left: 20px; - top: 5px; - display: inline-block; - visibility: hidden; - opacity: 0; - color: inherit; - font-size: 12px; - transition: visibility 0.1s, opacity 0.1s linear; - } - - &:active, - &:focus, - &:hover { - outline: none; - width: 35px; - - .fa.fa-caret-down { - visibility: visible; - opacity: 1; - } - } } /** diff --git a/app/assets/stylesheets/pages/profiles/preferences.scss b/app/assets/stylesheets/pages/profiles/preferences.scss index a353f301d07..45e62913f37 100644 --- a/app/assets/stylesheets/pages/profiles/preferences.scss +++ b/app/assets/stylesheets/pages/profiles/preferences.scss @@ -60,11 +60,11 @@ } &.ui-dark { - background-color: $theme-gray-900; + background-color: $gray-900; } &.ui-light { - background-color: $theme-gray-200; + background-color: $gray-200; } } diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index 004c49dd226..505f6e036e3 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -219,7 +219,7 @@ color: $gl-text-color-secondary; } - .project-tag-list { + .project-topic-list { font-size: $gl-font-size; font-weight: $gl-font-weight-normal; @@ -251,7 +251,7 @@ line-height: $gl-font-size-large; } - .project-tag-list, + .project-topic-list, .project-metadata { font-size: $gl-font-size-small; } @@ -273,7 +273,7 @@ } .access-request-link, - .project-tag-list { + .project-topic-list { padding-left: $gl-padding-8; border-left: 1px solid $gl-text-color-secondary; } @@ -459,7 +459,7 @@ margin-right: $gl-padding-4; margin-bottom: $gl-padding-4; color: $gl-text-color-secondary; - background-color: $theme-gray-100; + background-color: $gray-100; line-height: $gl-btn-line-height; &:hover { @@ -914,7 +914,7 @@ } .repository-language-bar-tooltip-share { - color: $theme-gray-400; + color: $gray-400; } pre.light-well { @@ -1025,8 +1025,10 @@ pre.light-well { margin: 0; } - @include media-breakpoint-up(md) { - .description { + .description { + line-height: 1.5; + + @include media-breakpoint-up(md) { color: $gl-text-color; } } diff --git a/app/assets/stylesheets/pages/reports.scss b/app/assets/stylesheets/pages/reports.scss index ecd51aa06a4..f7619ccbd20 100644 --- a/app/assets/stylesheets/pages/reports.scss +++ b/app/assets/stylesheets/pages/reports.scss @@ -96,7 +96,7 @@ } &.neutral svg { - color: $theme-gray-700; + color: $gray-700; } .ci-status-icon { diff --git a/app/assets/stylesheets/pages/settings.scss b/app/assets/stylesheets/pages/settings.scss index ccfa4e00a5b..c5b9d1f6885 100644 --- a/app/assets/stylesheets/pages/settings.scss +++ b/app/assets/stylesheets/pages/settings.scss @@ -297,7 +297,7 @@ .btn-clipboard { background-color: $white-light; - border: 1px solid $theme-gray-200; + border: 1px solid $gray-200; } .deploy-token-help-block { diff --git a/app/controllers/admin/requests_profiles_controller.rb b/app/controllers/admin/requests_profiles_controller.rb index 57f7d3e3951..89d4c4f18d9 100644 --- a/app/controllers/admin/requests_profiles_controller.rb +++ b/app/controllers/admin/requests_profiles_controller.rb @@ -11,7 +11,7 @@ class Admin::RequestsProfilesController < Admin::ApplicationController profile = Gitlab::RequestProfiler::Profile.find(clean_name) if profile - render html: profile.content + render html: profile.content.html_safe else redirect_to admin_requests_profiles_path, alert: 'Profile not found' end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index a8fc848c879..26cd5dc801f 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -177,11 +177,17 @@ class ApplicationController < ActionController::Base # hide existence of the resource, rather tell them they cannot access it using # the provided message status ||= message.present? ? :forbidden : :not_found + template = + if status == :not_found + "errors/not_found" + else + "errors/access_denied" + end respond_to do |format| format.any { head status } format.html do - render "errors/access_denied", + render template, layout: "errors", status: status, locals: { message: message } diff --git a/app/controllers/dashboard/projects_controller.rb b/app/controllers/dashboard/projects_controller.rb index f073b6de444..b1d224d026f 100644 --- a/app/controllers/dashboard/projects_controller.rb +++ b/app/controllers/dashboard/projects_controller.rb @@ -53,6 +53,9 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController # rubocop: disable CodeReuse/ActiveRecord def load_projects(finder_params) + @total_user_projects_count = ProjectsFinder.new(params: { non_public: true }, current_user: current_user).execute + @total_starred_projects_count = ProjectsFinder.new(params: { starred: true }, current_user: current_user).execute + projects = ProjectsFinder .new(params: finder_params, current_user: current_user) .execute diff --git a/app/controllers/explore/projects_controller.rb b/app/controllers/explore/projects_controller.rb index 778fdda8dbd..9f074690cbc 100644 --- a/app/controllers/explore/projects_controller.rb +++ b/app/controllers/explore/projects_controller.rb @@ -55,6 +55,9 @@ class Explore::ProjectsController < Explore::ApplicationController # rubocop: disable CodeReuse/ActiveRecord def load_projects + @total_user_projects_count = ProjectsFinder.new(params: { non_public: true }, current_user: current_user).execute + @total_starred_projects_count = ProjectsFinder.new(params: { starred: true }, current_user: current_user).execute + projects = ProjectsFinder.new(current_user: current_user, params: params) .execute .includes(:route, :creator, :group, namespace: [:route, :owner]) diff --git a/app/controllers/groups/children_controller.rb b/app/controllers/groups/children_controller.rb index d549f793ad7..236a19a8dc4 100644 --- a/app/controllers/groups/children_controller.rb +++ b/app/controllers/groups/children_controller.rb @@ -35,7 +35,7 @@ module Groups def setup_children(parent) @children = GroupDescendantsFinder.new(current_user: current_user, parent_group: parent, - params: params).execute + params: params.to_unsafe_h).execute @children = @children.page(params[:page]) end end diff --git a/app/controllers/import/base_controller.rb b/app/controllers/import/base_controller.rb index 042b6b1264f..9b45be6db99 100644 --- a/app/controllers/import/base_controller.rb +++ b/app/controllers/import/base_controller.rb @@ -18,6 +18,7 @@ class Import::BaseController < ApplicationController end # rubocop: enable CodeReuse/ActiveRecord + # deprecated: being replaced by app/services/import/base_service.rb def find_or_create_namespace(names, owner) names = params[:target_namespace].presence || names @@ -32,6 +33,7 @@ class Import::BaseController < ApplicationController current_user.namespace end + # deprecated: being replaced by app/services/import/base_service.rb def project_save_error(project) project.errors.full_messages.join(', ') end diff --git a/app/controllers/import/bitbucket_server_controller.rb b/app/controllers/import/bitbucket_server_controller.rb index 575c40d5f6f..87338488eba 100644 --- a/app/controllers/import/bitbucket_server_controller.rb +++ b/app/controllers/import/bitbucket_server_controller.rb @@ -40,7 +40,7 @@ class Import::BitbucketServerController < Import::BaseController else render json: { errors: 'This namespace has already been taken! Please choose another one.' }, status: :unprocessable_entity end - rescue BitbucketServer::Client::ServerError => e + rescue BitbucketServer::Connection::ConnectionError => e render json: { errors: "Unable to connect to server: #{e}" }, status: :unprocessable_entity end @@ -62,7 +62,7 @@ class Import::BitbucketServerController < Import::BaseController already_added_projects_names = @already_added_projects.pluck(:import_source) @repos.reject! { |repo| already_added_projects_names.include?(repo.browse_url) } - rescue BitbucketServer::Connection::ConnectionError, BitbucketServer::Client::ServerError => e + rescue BitbucketServer::Connection::ConnectionError => e flash[:alert] = "Unable to connect to server: #{e}" clear_session_data redirect_to new_import_bitbucket_server_path diff --git a/app/controllers/import/github_controller.rb b/app/controllers/import/github_controller.rb index d4c26fa0709..34c7dbdc2fe 100644 --- a/app/controllers/import/github_controller.rb +++ b/app/controllers/import/github_controller.rb @@ -39,28 +39,25 @@ class Import::GithubController < Import::BaseController end def create - repo = client.repo(params[:repo_id].to_i) - project_name = params[:new_name].presence || repo.name - namespace_path = params[:target_namespace].presence || current_user.namespace_path - target_namespace = find_or_create_namespace(namespace_path, current_user.namespace_path) - - if can?(current_user, :create_projects, target_namespace) - project = Gitlab::LegacyGithubImport::ProjectCreator - .new(repo, project_name, target_namespace, current_user, access_params, type: provider) - .execute(extra_project_attrs) - - if project.persisted? - render json: ProjectSerializer.new.represent(project) - else - render json: { errors: project_save_error(project) }, status: :unprocessable_entity - end + result = Import::GithubService.new(client, current_user, import_params).execute(access_params, provider) + + if result[:status] == :success + render json: ProjectSerializer.new.represent(result[:project]) else - render json: { errors: 'This namespace has already been taken! Please choose another one.' }, status: :unprocessable_entity + render json: { errors: result[:message] }, status: result[:http_status] end end private + def import_params + params.permit(permitted_import_params) + end + + def permitted_import_params + [:repo_id, :new_name, :target_namespace] + end + def client @client ||= Gitlab::LegacyGithubImport::Client.new(session[access_token_key], client_options) end @@ -124,10 +121,6 @@ class Import::GithubController < Import::BaseController {} end - def extra_project_attrs - {} - end - def extra_import_params {} end diff --git a/app/controllers/omniauth_callbacks_controller.rb b/app/controllers/omniauth_callbacks_controller.rb index 30be50d4595..f8e482937d5 100644 --- a/app/controllers/omniauth_callbacks_controller.rb +++ b/app/controllers/omniauth_callbacks_controller.rb @@ -75,6 +75,10 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController private def omniauth_flow(auth_module, identity_linker: nil) + if fragment = request.env.dig('omniauth.params', 'redirect_fragment').presence + store_redirect_fragment(fragment) + end + if current_user log_audit_event(current_user, with: oauth['provider']) @@ -189,4 +193,13 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController request_params = request.env['omniauth.params'] (request_params['remember_me'] == '1') if request_params.present? end + + def store_redirect_fragment(redirect_fragment) + key = stored_location_key_for(:user) + location = session[key] + if uri = parse_uri(location) + uri.fragment = redirect_fragment + store_location_for(:user, uri.to_s) + end + end end diff --git a/app/controllers/projects/artifacts_controller.rb b/app/controllers/projects/artifacts_controller.rb index 1a91e07b97f..2ef18d900f2 100644 --- a/app/controllers/projects/artifacts_controller.rb +++ b/app/controllers/projects/artifacts_controller.rb @@ -85,20 +85,15 @@ class Projects::ArtifactsController < Projects::ApplicationController end end - # rubocop: disable CodeReuse/ActiveRecord def build_from_id - project.builds.find_by(id: params[:job_id]) if params[:job_id] + project.builds.find_by_id(params[:job_id]) if params[:job_id] end - # rubocop: enable CodeReuse/ActiveRecord - # rubocop: disable CodeReuse/ActiveRecord def build_from_ref return unless @ref_name - builds = project.latest_successful_builds_for(@ref_name) - builds.find_by(name: params[:job]) + project.latest_successful_build_for(params[:job], @ref_name) end - # rubocop: enable CodeReuse/ActiveRecord def artifacts_file @artifacts_file ||= build&.artifacts_file_for_type(params[:file_type] || :archive) diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb index ff286c0ccf0..77672e7d9fc 100644 --- a/app/controllers/projects/blob_controller.rb +++ b/app/controllers/projects/blob_controller.rb @@ -93,7 +93,7 @@ class Projects::BlobController < Projects::ApplicationController @blob.load_all_data! @lines = @blob.present.highlight.lines - @form = UnfoldForm.new(params) + @form = UnfoldForm.new(params.to_unsafe_h) @lines = @lines[@form.since - 1..@form.to - 1].map(&:html_safe) diff --git a/app/controllers/projects/build_artifacts_controller.rb b/app/controllers/projects/build_artifacts_controller.rb index 7d4d566499c..4274c356227 100644 --- a/app/controllers/projects/build_artifacts_controller.rb +++ b/app/controllers/projects/build_artifacts_controller.rb @@ -44,18 +44,13 @@ class Projects::BuildArtifactsController < Projects::ApplicationController @job ||= job_from_id || job_from_ref end - # rubocop: disable CodeReuse/ActiveRecord def job_from_id - project.builds.find_by(id: params[:build_id]) if params[:build_id] + project.builds.find_by_id(params[:build_id]) if params[:build_id] end - # rubocop: enable CodeReuse/ActiveRecord - # rubocop: disable CodeReuse/ActiveRecord def job_from_ref return unless @ref_name - jobs = project.latest_successful_builds_for(@ref_name) - jobs.find_by(name: params[:job]) + project.latest_successful_build_for(params[:job], @ref_name) end - # rubocop: enable CodeReuse/ActiveRecord end diff --git a/app/controllers/projects/error_tracking_controller.rb b/app/controllers/projects/error_tracking_controller.rb new file mode 100644 index 00000000000..4596b6c91f2 --- /dev/null +++ b/app/controllers/projects/error_tracking_controller.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +class Projects::ErrorTrackingController < Projects::ApplicationController + before_action :check_feature_flag! + before_action :authorize_read_sentry_issue! + before_action :push_feature_flag_to_frontend + + POLLING_INTERVAL = 10_000 + + def index + respond_to do |format| + format.html + format.json do + set_polling_interval + render_index_json + end + end + end + + private + + def render_index_json + service = ErrorTracking::ListIssuesService.new(project, current_user) + result = service.execute + + unless result[:status] == :success + return render json: { message: result[:message] }, + status: result[:http_status] || :bad_request + end + + render json: { + errors: serialize_errors(result[:issues]), + external_url: service.external_url + } + end + + def set_polling_interval + Gitlab::PollingInterval.set_header(response, interval: POLLING_INTERVAL) + end + + def serialize_errors(errors) + ErrorTracking::ErrorSerializer + .new(project: project, user: current_user) + .represent(errors) + end + + def check_feature_flag! + render_404 unless Feature.enabled?(:error_tracking, project) + end + + def push_feature_flag_to_frontend + push_frontend_feature_flag(:error_tracking, current_user) + end +end diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index 21688e54481..e3e60665506 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -178,8 +178,6 @@ class Projects::IssuesController < Projects::ApplicationController end def import_csv - return render_404 unless Feature.enabled?(:issues_import_csv) - if uploader = UploadService.new(project, params[:file]).execute ImportIssuesCsvWorker.perform_async(current_user.id, project.id, uploader.upload.id) diff --git a/app/controllers/projects/jobs_controller.rb b/app/controllers/projects/jobs_controller.rb index bfbbcba883f..d5ce790e2d9 100644 --- a/app/controllers/projects/jobs_controller.rb +++ b/app/controllers/projects/jobs_controller.rb @@ -4,10 +4,10 @@ class Projects::JobsController < Projects::ApplicationController include SendFileUpload include ContinueParams - before_action :build, except: [:index, :cancel_all] + before_action :build, except: [:index] before_action :authorize_read_build! before_action :authorize_update_build!, - except: [:index, :show, :status, :raw, :trace, :cancel_all, :erase] + except: [:index, :show, :status, :raw, :trace, :erase] before_action :authorize_erase_build!, only: [:erase] before_action :authorize_use_build_terminal!, only: [:terminal, :terminal_websocket_authorize] before_action :verify_api_request!, only: :terminal_websocket_authorize @@ -39,16 +39,6 @@ class Projects::JobsController < Projects::ApplicationController end # rubocop: enable CodeReuse/ActiveRecord - def cancel_all - return access_denied! unless can?(current_user, :update_build, project) - - @project.builds.running_or_pending.each do |build| - build.cancel if can?(current_user, :update_build, build) - end - - redirect_to project_jobs_path(project) - end - # rubocop: disable CodeReuse/ActiveRecord def show @pipeline = @build.pipeline diff --git a/app/controllers/projects/milestones_controller.rb b/app/controllers/projects/milestones_controller.rb index 8e68014a30d..8bc59d8a305 100644 --- a/app/controllers/projects/milestones_controller.rb +++ b/app/controllers/projects/milestones_controller.rb @@ -144,7 +144,7 @@ class Projects::MilestonesController < Projects::ApplicationController def search_params if request.format.json? && project_group && can?(current_user, :read_group, project_group) - groups = project_group.self_and_ancestors_ids + groups = project_group.self_and_ancestors.select(:id) end params.permit(:state).merge(project_ids: @project.id, group_ids: groups) diff --git a/app/controllers/projects/pipelines_settings_controller.rb b/app/controllers/projects/pipelines_settings_controller.rb index 192e6d38f36..7f988110977 100644 --- a/app/controllers/projects/pipelines_settings_controller.rb +++ b/app/controllers/projects/pipelines_settings_controller.rb @@ -4,6 +4,6 @@ class Projects::PipelinesSettingsController < Projects::ApplicationController before_action :authorize_admin_pipeline! def show - redirect_to project_settings_ci_cd_path(@project, params: params) + redirect_to project_settings_ci_cd_path(@project, params: params.to_unsafe_h) end end diff --git a/app/controllers/projects/releases_controller.rb b/app/controllers/projects/releases_controller.rb index 62bdc84b41a..4c39ee4045f 100644 --- a/app/controllers/projects/releases_controller.rb +++ b/app/controllers/projects/releases_controller.rb @@ -4,16 +4,7 @@ class Projects::ReleasesController < Projects::ApplicationController # Authorize before_action :require_non_empty_project before_action :authorize_read_release! - before_action :check_releases_page_feature_flag def index end - - private - - def check_releases_page_feature_flag - return render_404 unless Feature.enabled?(:releases_page, @project) - - push_frontend_feature_flag(:releases_page, @project) - end end diff --git a/app/controllers/projects/serverless/functions_controller.rb b/app/controllers/projects/serverless/functions_controller.rb index 0af2b7ef343..39eca10134f 100644 --- a/app/controllers/projects/serverless/functions_controller.rb +++ b/app/controllers/projects/serverless/functions_controller.rb @@ -7,19 +7,17 @@ module Projects before_action :authorize_read_cluster! - INDEX_PRIMING_INTERVAL = 10_000 - INDEX_POLLING_INTERVAL = 30_000 + INDEX_PRIMING_INTERVAL = 15_000 + INDEX_POLLING_INTERVAL = 60_000 def index - finder = Projects::Serverless::FunctionsFinder.new(project.clusters) - respond_to do |format| format.json do functions = finder.execute if functions.any? Gitlab::PollingInterval.set_header(response, interval: INDEX_POLLING_INTERVAL) - render json: Projects::Serverless::ServiceSerializer.new(current_user: @current_user).represent(functions) + render json: serialize_function(functions) else Gitlab::PollingInterval.set_header(response, interval: INDEX_PRIMING_INTERVAL) head :no_content @@ -32,6 +30,29 @@ module Projects end end end + + def show + @service = serialize_function(finder.service(params[:environment_id], params[:id])) + return not_found if @service.nil? + + respond_to do |format| + format.json do + render json: @service + end + + format.html + end + end + + private + + def finder + Projects::Serverless::FunctionsFinder.new(project.clusters) + end + + def serialize_function(function) + Projects::Serverless::ServiceSerializer.new(current_user: @current_user, project: project).represent(function) + end end end end diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index b73a3fa6e01..1a69ec85d18 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -149,6 +149,18 @@ class IssuableFinder end end + def related_groups + if project? && project && project.group && Ability.allowed?(current_user, :read_group, project.group) + project.group.self_and_ancestors + elsif group + [group] + elsif current_user + Gitlab::ObjectHierarchy.new(current_user.authorized_groups, current_user.groups).all_objects + else + [] + end + end + def project? params[:project_id].present? end @@ -163,8 +175,10 @@ class IssuableFinder end # rubocop: disable CodeReuse/ActiveRecord - def projects(items = nil) - return @projects = project if project? + def projects + return @projects if defined?(@projects) + + return @projects = [project] if project? projects = if current_user && params[:authorized_only].presence && !current_user_related? @@ -459,7 +473,7 @@ class IssuableFinder elsif filter_by_any_milestone? items = items.any_milestone elsif filter_by_upcoming_milestone? - upcoming_ids = Milestone.upcoming_ids_by_projects(projects(items)) + upcoming_ids = Milestone.upcoming_ids(projects, related_groups) items = items.left_joins_milestones.where(milestone_id: upcoming_ids) elsif filter_by_started_milestone? items = items.left_joins_milestones.where('milestones.start_date <= NOW()') diff --git a/app/finders/milestones_finder.rb b/app/finders/milestones_finder.rb index 9c477978f60..fcd54b6106e 100644 --- a/app/finders/milestones_finder.rb +++ b/app/finders/milestones_finder.rb @@ -3,8 +3,8 @@ # Search for milestones # # params - Hash -# project_ids: Array of project ids or single project id. -# group_ids: Array of group ids or single group id. +# project_ids: Array of project ids or single project id or ActiveRecord relation. +# group_ids: Array of group ids or single group id or ActiveRecord relation. # order - Orders by field default due date asc. # title - filter by title. # state - filters by state. @@ -12,17 +12,13 @@ class MilestonesFinder include FinderMethods - attr_reader :params, :project_ids, :group_ids + attr_reader :params def initialize(params = {}) - @project_ids = Array(params[:project_ids]) - @group_ids = Array(params[:group_ids]) @params = params end def execute - return Milestone.none if project_ids.empty? && group_ids.empty? - items = Milestone.all items = by_groups_and_projects(items) items = by_title(items) @@ -34,7 +30,7 @@ class MilestonesFinder private def by_groups_and_projects(items) - items.for_projects_and_groups(project_ids, group_ids) + items.for_projects_and_groups(params[:project_ids], params[:group_ids]) end # rubocop: disable CodeReuse/ActiveRecord diff --git a/app/finders/projects/serverless/functions_finder.rb b/app/finders/projects/serverless/functions_finder.rb index 2b5d67e79d7..2f2816a4a08 100644 --- a/app/finders/projects/serverless/functions_finder.rb +++ b/app/finders/projects/serverless/functions_finder.rb @@ -15,11 +15,40 @@ module Projects clusters_with_knative_installed.exists? end + def service(environment_scope, name) + knative_service(environment_scope, name)&.first + end + private + def knative_service(environment_scope, name) + clusters_with_knative_installed.preload_knative.map do |cluster| + next if environment_scope != cluster.environment_scope + + services = cluster.application_knative.services_for(ns: cluster.platform_kubernetes&.actual_namespace) + .select { |svc| svc["metadata"]["name"] == name } + + add_metadata(cluster, services).first unless services.nil? + end + end + def knative_services clusters_with_knative_installed.preload_knative.map do |cluster| - cluster.application_knative.services_for(ns: cluster.platform_kubernetes&.actual_namespace) + services = cluster.application_knative.services_for(ns: cluster.platform_kubernetes&.actual_namespace) + add_metadata(cluster, services) unless services.nil? + end + end + + def add_metadata(cluster, services) + services.each do |s| + s["environment_scope"] = cluster.environment_scope + s["cluster_id"] = cluster.id + + if services.length == 1 + s["podcount"] = cluster.application_knative.service_pod_details( + cluster.platform_kubernetes&.actual_namespace, + s["metadata"]["name"]).length + end end end diff --git a/app/helpers/environments_helper.rb b/app/helpers/environments_helper.rb index 7b22bc8f98f..b3935ae350d 100644 --- a/app/helpers/environments_helper.rb +++ b/app/helpers/environments_helper.rb @@ -7,6 +7,15 @@ module EnvironmentsHelper } end + def environments_folder_list_view_data + { + "endpoint" => folder_project_environments_path(@project, @folder, format: :json), + "folder-name" => @folder, + "can-create-deployment" => can?(current_user, :create_deployment, @project).to_s, + "can-read-environment" => can?(current_user, :read_environment, @project).to_s + } + end + def metrics_data(project, environment) { "settings-path" => edit_project_service_path(project, 'prometheus'), diff --git a/app/helpers/projects/error_tracking_helper.rb b/app/helpers/projects/error_tracking_helper.rb new file mode 100644 index 00000000000..6daf2e21ca2 --- /dev/null +++ b/app/helpers/projects/error_tracking_helper.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module Projects::ErrorTrackingHelper + def error_tracking_data(project) + error_tracking_enabled = !!project.error_tracking_setting&.enabled? + + { + 'index-path' => project_error_tracking_index_path(project, + format: :json), + 'enable-error-tracking-link' => project_settings_operations_path(project), + 'error-tracking-enabled' => error_tracking_enabled.to_s, + 'illustration-path' => image_path('illustrations/cluster_popover.svg') + } + end +end diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index e67c327f7f8..ebbed08f78a 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -335,6 +335,7 @@ module ProjectsHelper builds: :read_build, clusters: :read_cluster, serverless: :read_cluster, + error_tracking: :read_sentry_issue, labels: :read_label, issues: :read_issue, project_members: :read_project_member, @@ -579,6 +580,7 @@ module ProjectsHelper environments clusters functions + error_tracking user gcp ] diff --git a/app/helpers/release_blog_post_helper.rb b/app/helpers/release_blog_post_helper.rb new file mode 100644 index 00000000000..31b5b7edc39 --- /dev/null +++ b/app/helpers/release_blog_post_helper.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +module ReleaseBlogPostHelper + def blog_post_url + Gitlab::ReleaseBlogPost.instance.blog_post_url + end +end diff --git a/app/models/clusters/applications/ingress.rb b/app/models/clusters/applications/ingress.rb index 7799f069742..7c15aaa4825 100644 --- a/app/models/clusters/applications/ingress.rb +++ b/app/models/clusters/applications/ingress.rb @@ -3,7 +3,7 @@ module Clusters module Applications class Ingress < ActiveRecord::Base - VERSION = '0.23.0'.freeze + VERSION = '1.1.2'.freeze self.table_name = 'clusters_applications_ingress' diff --git a/app/models/clusters/applications/knative.rb b/app/models/clusters/applications/knative.rb index c572c8bff44..8d79b041b64 100644 --- a/app/models/clusters/applications/knative.rb +++ b/app/models/clusters/applications/knative.rb @@ -41,6 +41,8 @@ module Clusters scope :for_cluster, -> (cluster) { where(cluster: cluster) } + after_save :clear_reactive_cache! + def chart 'knative/knative' end @@ -79,7 +81,7 @@ module Clusters end def calculate_reactive_cache - { services: read_services } + { services: read_services, pods: read_pods } end def ingress_service @@ -87,7 +89,7 @@ module Clusters end def services_for(ns: namespace) - return unless services + return [] unless services return [] unless ns services.select do |service| @@ -95,8 +97,22 @@ module Clusters end end + def service_pod_details(ns, service) + with_reactive_cache do |data| + data[:pods].select { |pod| filter_pods(pod, ns, service) } + end + end + private + def read_pods + cluster.kubeclient.core_client.get_pods.as_json + end + + def filter_pods(pod, namespace, service) + pod["metadata"]["namespace"] == namespace && pod["metadata"]["labels"]["serving.knative.dev/service"] == service + end + def read_services client.get_services.as_json rescue Kubeclient::ResourceNotFoundError diff --git a/app/models/concerns/manual_inverse_association.rb b/app/models/concerns/manual_inverse_association.rb index e18edd33ba7..ff61412767e 100644 --- a/app/models/concerns/manual_inverse_association.rb +++ b/app/models/concerns/manual_inverse_association.rb @@ -5,8 +5,8 @@ module ManualInverseAssociation class_methods do def manual_inverse_association(association, inverse) - define_method(association) do |*args| - super(*args).tap do |value| + define_method(association) do + super().tap do |value| next unless value child_association = value.association(inverse) diff --git a/app/models/error_tracking/project_error_tracking_setting.rb b/app/models/error_tracking/project_error_tracking_setting.rb index 632c64c2f1c..7f4947ba27a 100644 --- a/app/models/error_tracking/project_error_tracking_setting.rb +++ b/app/models/error_tracking/project_error_tracking_setting.rb @@ -2,13 +2,58 @@ module ErrorTracking class ProjectErrorTrackingSetting < ActiveRecord::Base + include ReactiveCaching + + self.reactive_cache_key = ->(setting) { [setting.class.model_name.singular, setting.project_id] } + belongs_to :project validates :api_url, length: { maximum: 255 }, public_url: true, url: { enforce_sanitization: true } + validate :validate_api_url_path + attr_encrypted :token, mode: :per_attribute_iv, key: Settings.attr_encrypted_db_key_base_truncated, algorithm: 'aes-256-gcm' + + after_save :clear_reactive_cache! + + def sentry_client + Sentry::Client.new(api_url, token) + end + + def sentry_external_url + self.class.extract_sentry_external_url(api_url) + end + + def list_sentry_issues(opts = {}) + with_reactive_cache('list_issues', opts.stringify_keys) do |result| + { issues: result } + end + end + + def calculate_reactive_cache(request, opts) + case request + when 'list_issues' + sentry_client.list_issues(**opts.symbolize_keys) + end + end + + # http://HOST/api/0/projects/ORG/PROJECT + # -> + # http://HOST/ORG/PROJECT + def self.extract_sentry_external_url(url) + url.sub('api/0/projects/', '') + end + + private + + def validate_api_url_path + unless URI(api_url).path.starts_with?('/api/0/projects') + errors.add(:api_url, 'path needs to start with /api/0/projects') + end + rescue URI::InvalidURIError + end end end diff --git a/app/models/external_issue.rb b/app/models/external_issue.rb index 4f73beaafc5..68b2353556e 100644 --- a/app/models/external_issue.rb +++ b/app/models/external_issue.rb @@ -3,6 +3,8 @@ class ExternalIssue include Referable + attr_reader :project + def initialize(issue_identifier, project) @issue_identifier, @project = issue_identifier, project end @@ -32,12 +34,8 @@ class ExternalIssue [self.class, to_s].hash end - def project - @project - end - def project_id - @project.id + project.id end def to_reference(_from = nil, full: nil) diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 5310f2ee765..7206d858dae 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -560,15 +560,19 @@ class MergeRequest < ActiveRecord::Base end def diff_refs - if persisted? - merge_request_diff.diff_refs - else - Gitlab::Diff::DiffRefs.new( - base_sha: diff_base_sha, - start_sha: diff_start_sha, - head_sha: diff_head_sha - ) - end + persisted? ? merge_request_diff.diff_refs : repository_diff_refs + end + + # Instead trying to fetch the + # persisted diff_refs, this method goes + # straight to the repository to get the + # most recent data possible. + def repository_diff_refs + Gitlab::Diff::DiffRefs.new( + base_sha: branch_merge_base_sha, + start_sha: target_branch_sha, + head_sha: source_branch_sha + ) end def branch_merge_base_sha @@ -1108,9 +1112,10 @@ class MergeRequest < ActiveRecord::Base end def update_head_pipeline - self.head_pipeline = find_actual_head_pipeline - - update_column(:head_pipeline_id, head_pipeline.id) if head_pipeline_id_changed? + find_actual_head_pipeline.try do |pipeline| + self.head_pipeline = pipeline + update_column(:head_pipeline_id, head_pipeline.id) if head_pipeline_id_changed? + end end def merge_request_pipeline_exists? diff --git a/app/models/milestone.rb b/app/models/milestone.rb index f55c39d9912..b21edce3aad 100644 --- a/app/models/milestone.rb +++ b/app/models/milestone.rb @@ -38,12 +38,14 @@ class Milestone < ActiveRecord::Base scope :closed, -> { with_state(:closed) } scope :for_projects, -> { where(group: nil).includes(:project) } - scope :for_projects_and_groups, -> (project_ids, group_ids) do - conditions = [] - conditions << arel_table[:project_id].in(project_ids) if project_ids&.compact&.any? - conditions << arel_table[:group_id].in(group_ids) if group_ids&.compact&.any? + scope :for_projects_and_groups, -> (projects, groups) do + projects = projects.compact if projects.is_a? Array + projects = [] if projects.nil? - where(conditions.reduce(:or)) + groups = groups.compact if groups.is_a? Array + groups = [] if groups.nil? + + where(project_id: projects).or(where(group_id: groups)) end scope :order_by_name_asc, -> { order(Arel::Nodes::Ascending.new(arel_table[:title].lower)) } @@ -133,18 +135,29 @@ class Milestone < ActiveRecord::Base @link_reference_pattern ||= super("milestones", /(?<milestone>\d+)/) end - def self.upcoming_ids_by_projects(projects) - rel = unscoped.of_projects(projects).active.where('due_date > ?', Time.now) + def self.upcoming_ids(projects, groups) + rel = unscoped + .for_projects_and_groups(projects, groups) + .active.where('milestones.due_date > NOW()') if Gitlab::Database.postgresql? - rel.order(:project_id, :due_date).select('DISTINCT ON (project_id) id') + rel.order(:project_id, :group_id, :due_date).select('DISTINCT ON (project_id, group_id) id') else + # We need to use MySQL's NULL-safe comparison operator `<=>` here + # because one of `project_id` or `group_id` is always NULL + join_clause = <<~HEREDOC + LEFT OUTER JOIN milestones earlier_milestones + ON milestones.project_id <=> earlier_milestones.project_id + AND milestones.group_id <=> earlier_milestones.group_id + AND milestones.due_date > earlier_milestones.due_date + AND earlier_milestones.due_date > NOW() + AND earlier_milestones.state = 'active' + HEREDOC + rel - .group(:project_id, :due_date, :id) - .having('due_date = MIN(due_date)') - .pluck(:id, :project_id, :due_date) - .uniq(&:second) - .map(&:first) + .joins(join_clause) + .where('earlier_milestones.id IS NULL') + .select(:id) end end @@ -178,7 +191,7 @@ class Milestone < ActiveRecord::Base return STATE_COUNT_HASH unless projects || groups counts = Milestone - .for_projects_and_groups(projects&.map(&:id), groups&.map(&:id)) + .for_projects_and_groups(projects, groups) .reorder(nil) .group(:state) .count @@ -262,8 +275,7 @@ class Milestone < ActiveRecord::Base if project relation = Milestone.for_projects_and_groups([project_id], [project.group&.id]) elsif group - project_ids = group.projects.map(&:id) - relation = Milestone.for_projects_and_groups(project_ids, [group.id]) + relation = Milestone.for_projects_and_groups(group.projects.select(:id), [group.id]) end title_exists = relation.find_by_title(title) diff --git a/app/models/pool_repository.rb b/app/models/pool_repository.rb index ad6a008dee8..34220c1b450 100644 --- a/app/models/pool_repository.rb +++ b/app/models/pool_repository.rb @@ -85,7 +85,11 @@ class PoolRepository < ActiveRecord::Base def unlink_repository(repository) object_pool.unlink_repository(repository.raw) - mark_obsolete unless member_projects.where.not(id: repository.project.id).exists? + if member_projects.where.not(id: repository.project.id).exists? + true + else + mark_obsolete + end end def object_pool diff --git a/app/models/project.rb b/app/models/project.rb index cab173503ce..15465d9b356 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -73,7 +73,7 @@ class Project < ActiveRecord::Base delegate :no_import?, to: :import_state, allow_nil: true default_value_for :archived, false - default_value_for :visibility_level, gitlab_config_features.visibility_level + default_value_for(:visibility_level) { Gitlab::CurrentSettings.default_project_visibility } default_value_for :resolve_outdated_diff_discussions, false default_value_for :container_registry_enabled, gitlab_config_features.container_registry default_value_for(:repository_storage) { Gitlab::CurrentSettings.pick_repository_storage } @@ -331,7 +331,7 @@ class Project < ActiveRecord::Base ports: ->(project) { project.persisted? ? VALID_MIRROR_PORTS : VALID_IMPORT_PORTS }, enforce_user: true }, if: [:external_import?, :import_url_changed?] validates :star_count, numericality: { greater_than_or_equal_to: 0 } - validate :check_limit, on: :create + 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) } @@ -647,19 +647,15 @@ class Project < ActiveRecord::Base end # ref can't be HEAD, can only be branch/tag name or SHA - def latest_successful_builds_for(ref = default_branch) + def latest_successful_build_for(job_name, ref = default_branch) latest_pipeline = ci_pipelines.latest_successful_for(ref) + return unless latest_pipeline - if latest_pipeline - latest_pipeline.builds.latest.with_artifacts_archive - else - builds.none - end + latest_pipeline.builds.latest.with_artifacts_archive.find_by(name: job_name) end - def latest_successful_build_for(job_name, ref = default_branch) - builds = latest_successful_builds_for(ref) - builds.find_by!(name: job_name) + def latest_successful_build_for!(job_name, ref = default_branch) + latest_successful_build_for(job_name, ref) || raise(ActiveRecord::RecordNotFound.new("Couldn't find job #{job_name}")) end def merge_base_commit(first_commit_id, second_commit_id) @@ -813,18 +809,22 @@ class Project < ActiveRecord::Base ::Gitlab::CurrentSettings.mirror_available end - def check_limit - unless creator.can_create_project? || namespace.kind == 'group' - projects_limit = creator.projects_limit + def check_personal_projects_limit + # Since this method is called as validation hook, `creator` might not be + # present. Since the validation for that will fail, we can just return + # early. + return if !creator || creator.can_create_project? || + namespace.kind == 'group' - if projects_limit == 0 - self.errors.add(:limit_reached, "Personal project creation is not allowed. Please contact your administrator with questions") + limit = creator.projects_limit + error = + if limit.zero? + _('Personal project creation is not allowed. Please contact your administrator with questions') else - self.errors.add(:limit_reached, "Your project limit is #{projects_limit} projects! Please contact your administrator to increase it") + _('Your project limit is %{limit} projects! Please contact your administrator to increase it') end - end - rescue - self.errors.add(:base, "Can't check your ability to create project") + + self.errors.add(:limit_reached, error % { limit: limit }) end def visibility_level_allowed_by_group @@ -1535,7 +1535,7 @@ class Project < ActiveRecord::Base end def pages_available? - Gitlab.config.pages.enabled && !namespace.subgroup? + Gitlab.config.pages.enabled end def remove_private_deploy_keys @@ -1606,24 +1606,7 @@ class Project < ActiveRecord::Base # rubocop: disable CodeReuse/ServiceClass def after_create_default_branch - return unless default_branch - - # Ensure HEAD points to the default branch in case it is not master - change_head(default_branch) - - if Gitlab::CurrentSettings.default_branch_protection != Gitlab::Access::PROTECTION_NONE && !ProtectedBranch.protected?(self, default_branch) - params = { - name: default_branch, - push_access_levels_attributes: [{ - access_level: Gitlab::CurrentSettings.default_branch_protection == Gitlab::Access::PROTECTION_DEV_CAN_PUSH ? Gitlab::Access::DEVELOPER : Gitlab::Access::MAINTAINER - }], - merge_access_levels_attributes: [{ - access_level: Gitlab::CurrentSettings.default_branch_protection == Gitlab::Access::PROTECTION_DEV_CAN_MERGE ? Gitlab::Access::DEVELOPER : Gitlab::Access::MAINTAINER - }] - } - - ProtectedBranches::CreateService.new(self, creator, params).execute(skip_authorization: true) - end + Projects::ProtectDefaultBranchService.new(self).execute end # rubocop: enable CodeReuse/ServiceClass @@ -2040,7 +2023,11 @@ class Project < ActiveRecord::Base end def leave_pool_repository - pool_repository&.unlink_repository(repository) + pool_repository&.unlink_repository(repository) && update_column(:pool_repository_id, nil) + end + + def link_pool_repository + pool_repository&.link_repository(repository) end private diff --git a/app/models/project_import_data.rb b/app/models/project_import_data.rb index 525725034a5..aa0c121fe99 100644 --- a/app/models/project_import_data.rb +++ b/app/models/project_import_data.rb @@ -30,4 +30,8 @@ class ProjectImportData < ActiveRecord::Base def merge_credentials(hash) self.credentials = credentials.to_h.merge(hash) unless hash.empty? end + + def clear_credentials + self.credentials = {} + end end diff --git a/app/models/project_services/teamcity_service.rb b/app/models/project_services/teamcity_service.rb index b8e17087db5..3245cd22e73 100644 --- a/app/models/project_services/teamcity_service.rb +++ b/app/models/project_services/teamcity_service.rb @@ -39,9 +39,7 @@ class TeamcityService < CiService end def help - 'The build configuration in Teamcity must use the build format '\ - 'number %build.vcs.number% '\ - 'you will also want to configure monitoring of all branches so merge '\ + 'You will want to configure monitoring of all branches so merge '\ 'requests build, that setting is in the vsc root advanced settings.' end @@ -70,7 +68,7 @@ class TeamcityService < CiService end def calculate_reactive_cache(sha, ref) - response = get_path("httpAuth/app/rest/builds/branch:unspecified:any,number:#{sha}") + response = get_path("httpAuth/app/rest/builds/branch:unspecified:any,revision:#{sha}") { build_page: read_build_page(response), commit_status: read_commit_status(response) } end diff --git a/app/models/releases/link.rb b/app/models/releases/link.rb index 766cb2efff2..6f639e5a7b2 100644 --- a/app/models/releases/link.rb +++ b/app/models/releases/link.rb @@ -6,7 +6,7 @@ module Releases belongs_to :release - validates :url, presence: true, url: true + validates :url, presence: true, url: true, uniqueness: { scope: :release } validates :name, presence: true, uniqueness: { scope: :release } scope :sorted, -> { order(created_at: :desc) } diff --git a/app/models/remote_mirror.rb b/app/models/remote_mirror.rb index a3fa67c72bf..5eba7ddd75c 100644 --- a/app/models/remote_mirror.rb +++ b/app/models/remote_mirror.rb @@ -61,7 +61,10 @@ class RemoteMirror < ActiveRecord::Base timestamp = Time.now remote_mirror.update!( - last_update_at: timestamp, last_successful_update_at: timestamp, last_error: nil + last_update_at: timestamp, + last_successful_update_at: timestamp, + last_error: nil, + error_notification_sent: false ) end @@ -179,6 +182,10 @@ class RemoteMirror < ActiveRecord::Base project.repository.add_remote(remote_name, remote_url) end + def after_sent_notification + update_column(:error_notification_sent, true) + end + private def store_credentials @@ -221,7 +228,8 @@ class RemoteMirror < ActiveRecord::Base last_error: nil, last_update_at: nil, last_successful_update_at: nil, - update_status: 'finished' + update_status: 'finished', + error_notification_sent: false ) end diff --git a/app/models/repository.rb b/app/models/repository.rb index b19ae2e0e6a..b47238b52f1 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -1072,19 +1072,11 @@ class Repository end def cache - @cache ||= if is_wiki - Gitlab::RepositoryCache.new(self, extra_namespace: 'wiki') - else - Gitlab::RepositoryCache.new(self) - end + @cache ||= Gitlab::RepositoryCache.new(self) end def request_store_cache - @request_store_cache ||= if is_wiki - Gitlab::RepositoryCache.new(self, extra_namespace: 'wiki', backend: Gitlab::SafeRequestStore) - else - Gitlab::RepositoryCache.new(self, backend: Gitlab::SafeRequestStore) - end + @request_store_cache ||= Gitlab::RepositoryCache.new(self, backend: Gitlab::SafeRequestStore) end def tags_sorted_by_committed_date diff --git a/app/models/ssh_host_key.rb b/app/models/ssh_host_key.rb index b6844dbe870..99a0c54a26a 100644 --- a/app/models/ssh_host_key.rb +++ b/app/models/ssh_host_key.rb @@ -52,6 +52,11 @@ class SshHostKey @compare_host_keys = compare_host_keys end + # Needed for reactive caching + def self.primary_key + 'id' + end + def id [project.id, url].join(':') end diff --git a/app/models/suggestion.rb b/app/models/suggestion.rb index c76b8e71507..7eee4fbbe5f 100644 --- a/app/models/suggestion.rb +++ b/app/models/suggestion.rb @@ -5,8 +5,7 @@ class Suggestion < ApplicationRecord validates :note, presence: true validates :commit_id, presence: true, if: :applied? - delegate :original_position, :position, :diff_file, - :noteable, to: :note + delegate :original_position, :position, :noteable, to: :note def project noteable.source_project @@ -16,6 +15,15 @@ class Suggestion < ApplicationRecord noteable.source_branch end + def file_path + position.file_path + end + + def diff_file + repository = project.repository + position.diff_file(repository) + end + # For now, suggestions only serve as a way to send patches that # will change a single line (being able to apply multiple in the same place), # which explains `from_line` and `to_line` being the same line. diff --git a/app/policies/base_policy.rb b/app/policies/base_policy.rb index 0d0f1c28bad..72de04203a6 100644 --- a/app/policies/base_policy.rb +++ b/app/policies/base_policy.rb @@ -7,6 +7,10 @@ class BasePolicy < DeclarativePolicy::Base with_options scope: :user, score: 0 condition(:admin) { @user&.admin? } + desc "User has access to all private groups & projects" + with_options scope: :user, score: 0 + condition(:full_private_access) { @user&.full_private_access? } + with_options scope: :user, score: 0 condition(:external_user) { @user.nil? || @user.external? } diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb index d70417e710e..12f9f29dcc1 100644 --- a/app/policies/project_policy.rb +++ b/app/policies/project_policy.rb @@ -200,6 +200,7 @@ class ProjectPolicy < BasePolicy enable :read_environment enable :read_deployment enable :read_merge_request + enable :read_sentry_issue end # We define `:public_user_access` separately because there are cases in gitlab-ee diff --git a/app/policies/project_snippet_policy.rb b/app/policies/project_snippet_policy.rb index 288bf070cfc..7dafa33bb99 100644 --- a/app/policies/project_snippet_policy.rb +++ b/app/policies/project_snippet_policy.rb @@ -5,13 +5,12 @@ class ProjectSnippetPolicy < BasePolicy desc "Snippet is public" condition(:public_snippet, scope: :subject) { @subject.public? } + condition(:internal_snippet, scope: :subject) { @subject.internal? } condition(:private_snippet, scope: :subject) { @subject.private? } condition(:public_project, scope: :subject) { @subject.project.public? } condition(:is_author) { @user && @subject.author == @user } - condition(:internal, scope: :subject) { @subject.internal? } - # We have to check both project feature visibility and a snippet visibility and take the stricter one # This will be simplified - check https://gitlab.com/gitlab-org/gitlab-ce/issues/27573 rule { ~can?(:read_project) }.policy do @@ -26,13 +25,13 @@ class ProjectSnippetPolicy < BasePolicy # is used to hide/show various snippet-related controls, so we can't just move # all of the handling here. rule do - all?(private_snippet | (internal & external_user), + all?(private_snippet | (internal_snippet & external_user), ~project.guest, - ~admin, - ~is_author) + ~is_author, + ~full_private_access) end.prevent :read_project_snippet - rule { internal & ~is_author & ~admin }.policy do + rule { internal_snippet & ~is_author & ~admin }.policy do prevent :update_project_snippet prevent :admin_project_snippet end diff --git a/app/presenters/project_presenter.rb b/app/presenters/project_presenter.rb index 9bd64ea217e..ea1d941cf83 100644 --- a/app/presenters/project_presenter.rb +++ b/app/presenters/project_presenter.rb @@ -13,7 +13,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated presents :project AnchorData = Struct.new(:is_link, :label, :link, :class_modifier, :icon) - MAX_TAGS_TO_SHOW = 3 + MAX_TOPICS_TO_SHOW = 3 def statistic_icon(icon_name = 'plus-square-o') sprite_icon(icon_name, size: 16, css_class: 'icon append-right-4') @@ -310,20 +310,20 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated end end - def tags_to_show - project.tag_list.take(MAX_TAGS_TO_SHOW) # rubocop: disable CodeReuse/ActiveRecord + def topics_to_show + project.tag_list.take(MAX_TOPICS_TO_SHOW) # rubocop: disable CodeReuse/ActiveRecord end - def count_of_extra_tags_not_shown - if project.tag_list.count > MAX_TAGS_TO_SHOW - project.tag_list.count - MAX_TAGS_TO_SHOW + def count_of_extra_topics_not_shown + if project.tag_list.count > MAX_TOPICS_TO_SHOW + project.tag_list.count - MAX_TOPICS_TO_SHOW else 0 end end - def has_extra_tags? - count_of_extra_tags_not_shown > 0 + def has_extra_topics? + count_of_extra_topics_not_shown > 0 end private diff --git a/app/serializers/base_serializer.rb b/app/serializers/base_serializer.rb index 7b65bd22f54..4744a7c1cc8 100644 --- a/app/serializers/base_serializer.rb +++ b/app/serializers/base_serializer.rb @@ -16,11 +16,11 @@ class BaseSerializer .as_json end - def self.entity(entity_class) - @entity_class ||= entity_class - end + class << self + attr_reader :entity_class - def self.entity_class - @entity_class + def entity(entity_class) + @entity_class ||= entity_class + end end end diff --git a/app/serializers/error_tracking/error_entity.rb b/app/serializers/error_tracking/error_entity.rb new file mode 100644 index 00000000000..91388e7c3ad --- /dev/null +++ b/app/serializers/error_tracking/error_entity.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +module ErrorTracking + class ErrorEntity < Grape::Entity + expose :id, :title, :type, :user_count, :count, + :first_seen, :last_seen, :message, :culprit, + :external_url, :project_id, :project_name, :project_slug, + :short_id, :status, :frequency + end +end diff --git a/app/serializers/error_tracking/error_serializer.rb b/app/serializers/error_tracking/error_serializer.rb new file mode 100644 index 00000000000..ff9a645eb16 --- /dev/null +++ b/app/serializers/error_tracking/error_serializer.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +module ErrorTracking + class ErrorSerializer < BaseSerializer + entity ErrorEntity + end +end diff --git a/app/serializers/merge_request_diff_entity.rb b/app/serializers/merge_request_diff_entity.rb index 433bfe60474..7e3053e5881 100644 --- a/app/serializers/merge_request_diff_entity.rb +++ b/app/serializers/merge_request_diff_entity.rb @@ -24,6 +24,14 @@ class MergeRequestDiffEntity < Grape::Entity short_sha(merge_request_diff.head_commit_sha) end + expose :base_version_path do |merge_request_diff| + project = merge_request.target_project + + next unless project + + merge_request_version_path(project, merge_request, merge_request_diff) + end + expose :version_path do |merge_request_diff| start_sha = options[:start_sha] project = merge_request.target_project diff --git a/app/serializers/projects/serverless/service_entity.rb b/app/serializers/projects/serverless/service_entity.rb index 4f1f62d145b..c98dc1a1c4a 100644 --- a/app/serializers/projects/serverless/service_entity.rb +++ b/app/serializers/projects/serverless/service_entity.rb @@ -13,6 +13,25 @@ module Projects service.dig('metadata', 'namespace') end + expose :environment_scope do |service| + service.dig('environment_scope') + end + + expose :cluster_id do |service| + service.dig('cluster_id') + end + + expose :detail_url do |service| + project_serverless_path( + request.project, + service.dig('environment_scope'), + service.dig('metadata', 'name')) + end + + expose :podcount do |service| + service.dig('podcount') + end + expose :created_at do |service| service.dig('metadata', 'creationTimestamp') end @@ -22,11 +41,24 @@ module Projects end expose :description do |service| - service.dig('spec', 'runLatest', 'configuration', 'revisionTemplate', 'metadata', 'annotations', 'Description') + service.dig( + 'spec', + 'runLatest', + 'configuration', + 'revisionTemplate', + 'metadata', + 'annotations', + 'Description') end expose :image do |service| - service.dig('spec', 'runLatest', 'configuration', 'build', 'template', 'name') + service.dig( + 'spec', + 'runLatest', + 'configuration', + 'build', + 'template', + 'name') end end end diff --git a/app/services/ci/destroy_pipeline_service.rb b/app/services/ci/destroy_pipeline_service.rb index 13f892aabb8..5c4a34043c1 100644 --- a/app/services/ci/destroy_pipeline_service.rb +++ b/app/services/ci/destroy_pipeline_service.rb @@ -5,8 +5,6 @@ module Ci def execute(pipeline) raise Gitlab::Access::AccessDeniedError unless can?(current_user, :destroy_pipeline, pipeline) - AuditEventService.new(current_user, pipeline).security_event - pipeline.destroy! end end diff --git a/app/services/error_tracking/list_issues_service.rb b/app/services/error_tracking/list_issues_service.rb new file mode 100644 index 00000000000..4cc35cfa4a8 --- /dev/null +++ b/app/services/error_tracking/list_issues_service.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +module ErrorTracking + class ListIssuesService < ::BaseService + DEFAULT_ISSUE_STATUS = 'unresolved' + DEFAULT_LIMIT = 20 + + def execute + return error('not enabled') unless enabled? + return error('access denied') unless can_read? + + result = project_error_tracking_setting + .list_sentry_issues(issue_status: issue_status, limit: limit) + + # our results are not yet ready + unless result + return error('not ready', :no_content) + end + + success(issues: result[:issues]) + end + + def external_url + project_error_tracking_setting&.sentry_external_url + end + + private + + def project_error_tracking_setting + project.error_tracking_setting + end + + def issue_status + params[:issue_status] || DEFAULT_ISSUE_STATUS + end + + def limit + params[:limit] || DEFAULT_LIMIT + end + + def enabled? + project_error_tracking_setting&.enabled? + end + + def can_read? + can?(current_user, :read_sentry_issue, project) + end + end +end diff --git a/app/services/import/base_service.rb b/app/services/import/base_service.rb new file mode 100644 index 00000000000..2683c75e41f --- /dev/null +++ b/app/services/import/base_service.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +module Import + class BaseService < ::BaseService + def initialize(client, user, params) + @client = client + @current_user = user + @params = params + end + + private + + def find_or_create_namespace(namespace, owner) + namespace = params[:target_namespace].presence || namespace + + return current_user.namespace if namespace == owner + + group = Groups::NestedCreateService.new(current_user, group_path: namespace).execute + + group.errors.any? ? current_user.namespace : group + rescue => e + Gitlab::AppLogger.error(e) + + current_user.namespace + end + + def project_save_error(project) + project.errors.full_messages.join(', ') + end + + def success(project) + super().merge(project: project, status: :success) + end + end +end diff --git a/app/services/import/github_service.rb b/app/services/import/github_service.rb new file mode 100644 index 00000000000..a2533683da9 --- /dev/null +++ b/app/services/import/github_service.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +module Import + class GithubService < Import::BaseService + attr_accessor :client + attr_reader :params, :current_user + + def execute(access_params, provider) + unless authorized? + return error('This namespace has already been taken! Please choose another one.', :unprocessable_entity) + end + + project = Gitlab::LegacyGithubImport::ProjectCreator + .new(repo, project_name, target_namespace, current_user, access_params, type: provider) + .execute(extra_project_attrs) + + if project.persisted? + success(project) + else + error(project_save_error(project), :unprocessable_entity) + end + end + + def repo + @repo ||= client.repo(params[:repo_id].to_i) + end + + def project_name + @project_name ||= params[:new_name].presence || repo.name + end + + def namespace_path + @namespace_path ||= params[:target_namespace].presence || current_user.namespace_path + end + + def target_namespace + @target_namespace ||= find_or_create_namespace(namespace_path, current_user.namespace_path) + end + + def extra_project_attrs + {} + end + + def authorized? + can?(current_user, :create_projects, target_namespace) + end + end +end diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb index c7e7bb55e4b..805bb5b510d 100644 --- a/app/services/issuable_base_service.rb +++ b/app/services/issuable_base_service.rb @@ -61,10 +61,10 @@ class IssuableBaseService < BaseService return unless milestone_id params[:milestone_id] = '' if milestone_id == IssuableFinder::NONE - group_ids = project.group&.self_and_ancestors&.pluck(:id) + groups = project.group&.self_and_ancestors&.select(:id) milestone = - Milestone.for_projects_and_groups([project.id], group_ids).find_by_id(milestone_id) + Milestone.for_projects_and_groups([project.id], groups).find_by_id(milestone_id) params[:milestone_id] = '' unless milestone end diff --git a/app/services/labels/create_service.rb b/app/services/labels/create_service.rb index fe34be41ac1..db710bac900 100644 --- a/app/services/labels/create_service.rb +++ b/app/services/labels/create_service.rb @@ -3,7 +3,7 @@ module Labels class CreateService < Labels::BaseService def initialize(params = {}) - @params = params.dup.with_indifferent_access + @params = params.to_h.dup.with_indifferent_access end # returns the created label diff --git a/app/services/labels/update_service.rb b/app/services/labels/update_service.rb index c3a720a1c66..e563447c64c 100644 --- a/app/services/labels/update_service.rb +++ b/app/services/labels/update_service.rb @@ -3,7 +3,7 @@ module Labels class UpdateService < Labels::BaseService def initialize(params = {}) - @params = params.dup.with_indifferent_access + @params = params.to_h.dup.with_indifferent_access end # returns the updated label diff --git a/app/services/lfs/locks_finder_service.rb b/app/services/lfs/locks_finder_service.rb index 4a5b2a52921..192ce3d3c2a 100644 --- a/app/services/lfs/locks_finder_service.rb +++ b/app/services/lfs/locks_finder_service.rb @@ -12,7 +12,7 @@ module Lfs # rubocop: disable CodeReuse/ActiveRecord def find_locks - options = params.slice(:id, :path).compact.symbolize_keys + options = params.slice(:id, :path).to_h.compact.symbolize_keys project.lfs_file_locks.where(options) end diff --git a/app/services/milestones/promote_service.rb b/app/services/milestones/promote_service.rb index 39071b5dc14..cbe5996e8ca 100644 --- a/app/services/milestones/promote_service.rb +++ b/app/services/milestones/promote_service.rb @@ -82,11 +82,9 @@ module Milestones end # rubocop: enable CodeReuse/ActiveRecord - # rubocop: disable CodeReuse/ActiveRecord def group_project_ids - @group_project_ids ||= group.projects.pluck(:id) + group.projects.select(:id) end - # rubocop: enable CodeReuse/ActiveRecord def raise_error(message) raise PromoteMilestoneError, "Promotion failed - #{message}" diff --git a/app/services/projects/autocomplete_service.rb b/app/services/projects/autocomplete_service.rb index 61f6402a810..3dad90188cf 100644 --- a/app/services/projects/autocomplete_service.rb +++ b/app/services/projects/autocomplete_service.rb @@ -14,7 +14,7 @@ module Projects order: { due_date: :asc, title: :asc } } - finder_params[:group_ids] = @project.group.self_and_ancestors_ids if @project.group + finder_params[:group_ids] = @project.group.self_and_ancestors.select(:id) if @project.group MilestonesFinder.new(finder_params).execute.select([:iid, :title]) end diff --git a/app/services/projects/protect_default_branch_service.rb b/app/services/projects/protect_default_branch_service.rb new file mode 100644 index 00000000000..245490791bf --- /dev/null +++ b/app/services/projects/protect_default_branch_service.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true + +module Projects + # Service class that can be used to execute actions necessary after creating a + # default branch. + class ProtectDefaultBranchService + attr_reader :project, :default_branch_protection + + # @param [Project] project + def initialize(project) + @project = project + + @default_branch_protection = Gitlab::Access::BranchProtection + .new(Gitlab::CurrentSettings.default_branch_protection) + end + + def execute + protect_default_branch if default_branch + end + + def protect_default_branch + # Ensure HEAD points to the default branch in case it is not master + project.change_head(default_branch) + + create_protected_branch if protect_branch? + end + + def create_protected_branch + params = { + name: default_branch, + push_access_levels_attributes: [{ access_level: push_access_level }], + merge_access_levels_attributes: [{ access_level: merge_access_level }] + } + + # The creator of the project is always allowed to create protected + # branches, so we skip the authorization check in this service class. + ProtectedBranches::CreateService + .new(project, project.creator, params) + .execute(skip_authorization: true) + end + + def protect_branch? + default_branch_protection.any? && + !ProtectedBranch.protected?(project, default_branch) + end + + def default_branch + project.default_branch + end + + def push_access_level + if default_branch_protection.developer_can_push? + Gitlab::Access::DEVELOPER + else + Gitlab::Access::MAINTAINER + end + end + + def merge_access_level + if default_branch_protection.developer_can_merge? + Gitlab::Access::DEVELOPER + else + Gitlab::Access::MAINTAINER + end + end + end +end diff --git a/app/services/suggestions/apply_service.rb b/app/services/suggestions/apply_service.rb index d931d528c86..cc47b46b527 100644 --- a/app/services/suggestions/apply_service.rb +++ b/app/services/suggestions/apply_service.rb @@ -11,6 +11,10 @@ module Suggestions return error('Suggestion is not appliable') end + unless latest_diff_refs?(suggestion) + return error('The file has been changed') + end + params = file_update_params(suggestion) result = ::Files::UpdateService.new(suggestion.project, @current_user, params).execute @@ -19,30 +23,44 @@ module Suggestions end result + rescue Files::UpdateService::FileChangedError + error('The file has been changed') end private - def file_update_params(suggestion) - diff_file = suggestion.diff_file + # Checks whether the latest diff refs for the branch matches with + # the position refs we're using to update the file content. Since + # the persisted refs are updated async (for MergeRequest), + # it's more consistent to fetch this data directly from the repository. + def latest_diff_refs?(suggestion) + suggestion.position.diff_refs == suggestion.noteable.repository_diff_refs + end - file_path = diff_file.file_path - branch_name = suggestion.noteable.source_branch - file_content = new_file_content(suggestion) + def file_update_params(suggestion) + blob = suggestion.diff_file.new_blob + file_path = suggestion.file_path + branch_name = suggestion.branch + file_content = new_file_content(suggestion, blob) commit_message = "Apply suggestion to #{file_path}" + file_last_commit = + Gitlab::Git::Commit.last_for_path(suggestion.project.repository, + blob.commit_id, + blob.path) + { file_path: file_path, branch_name: branch_name, start_branch: branch_name, commit_message: commit_message, - file_content: file_content + file_content: file_content, + last_commit_sha: file_last_commit&.id } end - def new_file_content(suggestion) + def new_file_content(suggestion, blob) range = suggestion.from_line_index..suggestion.to_line_index - blob = suggestion.diff_file.new_blob blob.load_all_data! content = blob.data.lines diff --git a/app/services/users/update_service.rb b/app/services/users/update_service.rb index af4fe1aebb9..0b00bd135eb 100644 --- a/app/services/users/update_service.rb +++ b/app/services/users/update_service.rb @@ -55,7 +55,7 @@ module Users params.reject! { |key, _| read_only.include?(key.to_sym) } end - @user.assign_attributes(params) if params.any? + @user.assign_attributes(params) unless params.empty? end end end diff --git a/app/uploaders/records_uploads.rb b/app/uploaders/records_uploads.rb index 0efca895a50..9a243e07936 100644 --- a/app/uploaders/records_uploads.rb +++ b/app/uploaders/records_uploads.rb @@ -23,13 +23,23 @@ module RecordsUploads return unless model return unless file && file.exists? - Upload.transaction do - uploads.where(path: upload_path).delete_all - upload.delete if upload - - self.upload = build_upload.tap(&:save!) + # MySQL InnoDB may encounter a deadlock if a deletion and an + # insert is in the same transaction due to its next-key locking + # algorithm, so we need to skip the transaction. + # https://gitlab.com/gitlab-org/gitlab-ce/issues/55161#note_131556351 + if Gitlab::Database.mysql? + readd_upload + else + Upload.transaction { readd_upload } end end + + def readd_upload + uploads.where(path: upload_path).delete_all + upload.delete if upload + + self.upload = build_upload.tap(&:save!) + end # rubocop: enable CodeReuse/ActiveRecord def upload_path diff --git a/app/views/admin/application_settings/_ci_cd.html.haml b/app/views/admin/application_settings/_ci_cd.html.haml index fdaad1cf181..c99d7e9b8e9 100644 --- a/app/views/admin/application_settings/_ci_cd.html.haml +++ b/app/views/admin/application_settings/_ci_cd.html.haml @@ -13,7 +13,7 @@ = s_('CICD|The Auto DevOps pipeline will run if no alternative CI configuration file is found.') = link_to _('More information'), help_page_path('topics/autodevops/index.md'), target: '_blank' .form-group - = f.label :auto_devops_domain, class: 'label-bold' + = f.label :auto_devops_domain, s_('AdminSettings|Auto DevOps domain'), class: 'label-bold' = f.text_field :auto_devops_domain, class: 'form-control', placeholder: 'domain.com' .form-text.text-muted = s_("AdminSettings|Specify a domain to use by default for every project's Auto Review Apps and Auto Deploy stages.") diff --git a/app/views/clusters/clusters/_integration_form.html.haml b/app/views/clusters/clusters/_integration_form.html.haml index 5e451f60c9d..4c47e11927e 100644 --- a/app/views/clusters/clusters/_integration_form.html.haml +++ b/app/views/clusters/clusters/_integration_form.html.haml @@ -13,19 +13,19 @@ = sprite_icon('status_failed_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-unchecked') .form-text.text-muted= s_('ClusterIntegration|Enable or disable GitLab\'s connection to your Kubernetes cluster.') - - if has_multiple_clusters? - .form-group - %h5= s_('ClusterIntegration|Environment scope') + .form-group + %h5= s_('ClusterIntegration|Environment scope') + - if has_multiple_clusters? = field.text_field :environment_scope, class: 'col-md-6 form-control js-select-on-focus', placeholder: s_('ClusterIntegration|Environment scope') .form-text.text-muted= s_("ClusterIntegration|Choose which of your environments will use this cluster.") + - else + = text_field_tag :environment_scope, '*', class: 'col-md-6 form-control disabled', placeholder: s_('ClusterIntegration|Environment scope'), disabled: true + - environment_scope_url = 'https://docs.gitlab.com/ee/user/project/clusters/#setting-the-environment-scope-premium' + - environment_scope_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: environment_scope_url } + .form-text.text-muted + %code * + = s_("ClusterIntegration| is the default environment scope for this cluster. This means that all jobs, regardless of their environment, will use this cluster. %{environment_scope_start}More information%{environment_scope_end}").html_safe % { environment_scope_start: environment_scope_start, environment_scope_end: '</a>'.html_safe } - if can?(current_user, :update_cluster, @cluster) .form-group = field.submit _('Save changes'), class: 'btn btn-success' - - - unless has_multiple_clusters? - %h5= s_('ClusterIntegration|Environment scope') - %p - %code * - is the default environment scope for this cluster. This means that all jobs, regardless of their environment, will use this cluster. - = link_to 'More information', ('https://docs.gitlab.com/ee/user/project/clusters/#setting-the-environment-scope') diff --git a/app/views/dashboard/_projects_head.html.haml b/app/views/dashboard/_projects_head.html.haml index 1050945b15a..ae67192cbcd 100644 --- a/app/views/dashboard/_projects_head.html.haml +++ b/app/views/dashboard/_projects_head.html.haml @@ -15,9 +15,11 @@ = nav_link(page: [dashboard_projects_path, root_path]) do = link_to dashboard_projects_path, class: 'shortcuts-activity', data: {placement: 'right'} do 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 + %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 diff --git a/app/views/devise/sessions/new.html.haml b/app/views/devise/sessions/new.html.haml index 34d4293bd45..30ed7ed6b29 100644 --- a/app/views/devise/sessions/new.html.haml +++ b/app/views/devise/sessions/new.html.haml @@ -1,6 +1,6 @@ - page_title "Sign in" -%div +#signin-container - if form_based_providers.any? = render 'devise/shared/tabs_ldap' - else diff --git a/app/views/groups/runners/_group_runners.html.haml b/app/views/groups/runners/_group_runners.html.haml index bcfb6d99716..806a24e2bbc 100644 --- a/app/views/groups/runners/_group_runners.html.haml +++ b/app/views/groups/runners/_group_runners.html.haml @@ -21,6 +21,6 @@ - else %h4.underlined-title - = _('Available group Runners : %{runners}.').html_safe % { runners: @group.runners.count } + = _('Available group Runners: %{runners}').html_safe % { runners: @group.runners.count } %ul.bordered-list = render partial: 'groups/runners/runner', collection: @group.runners, as: :runner diff --git a/app/views/layouts/header/_help_dropdown.html.haml b/app/views/layouts/header/_help_dropdown.html.haml index 04409408ce0..513902890af 100644 --- a/app/views/layouts/header/_help_dropdown.html.haml +++ b/app/views/layouts/header/_help_dropdown.html.haml @@ -1,9 +1,13 @@ +- show_blog_link = current_user_menu?(:help) && blog_post_url.present? %ul - if current_user_menu?(:help) %li = link_to _("Help"), help_path %li.divider + - if show_blog_link %li - = link_to _("Submit feedback"), "https://about.gitlab.com/submit-feedback" + = link_to _("What's new?"), blog_post_url + %li + = 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' diff --git a/app/views/layouts/nav/sidebar/_project.html.haml b/app/views/layouts/nav/sidebar/_project.html.haml index e516c76400a..4b67069d9ac 100644 --- a/app/views/layouts/nav/sidebar/_project.html.haml +++ b/app/views/layouts/nav/sidebar/_project.html.haml @@ -29,7 +29,7 @@ = link_to activity_project_path(@project), title: _('Activity'), class: 'shortcuts-project-activity' do %span= _('Activity') - - if project_nav_tab?(:releases) && Feature.enabled?(:releases_page, @project) + - if project_nav_tab?(:releases) = nav_link(controller: :releases) do = link_to project_releases_path(@project), title: _('Releases'), class: 'shortcuts-project-releases' do %span= _('Releases') @@ -43,7 +43,7 @@ - if project_nav_tab? :files = nav_link(controller: sidebar_repository_paths) do - = link_to project_tree_path(@project), class: 'shortcuts-tree' do + = link_to project_tree_path(@project), class: 'shortcuts-tree qa-project-menu-repo' do .nav-icon-container = sprite_icon('doc-text') %span.nav-item-name @@ -64,7 +64,7 @@ = _('Commits') = nav_link(html_options: {class: branches_tab_class}) do - = link_to project_branches_path(@project) do + = link_to project_branches_path(@project), class: 'qa-branches-link' do = _('Branches') = nav_link(controller: [:tags]) do @@ -201,7 +201,7 @@ - if project_nav_tab? :operations = nav_link(controller: sidebar_operations_paths) do - = link_to sidebar_operations_link_path, class: 'shortcuts-operations' do + = link_to sidebar_operations_link_path, class: 'shortcuts-operations qa-link-operations' do .nav-icon-container = sprite_icon('cloud-gear') %span.nav-item-name @@ -227,6 +227,12 @@ %span = _('Environments') + - if project_nav_tab?(:error_tracking) && Feature.enabled?(:error_tracking, @project) + = nav_link(controller: :error_tracking) do + = link_to project_error_tracking_index_path(@project), title: _('Error Tracking'), class: 'shortcuts-tracking qa-operations-tracking-link' do + %span + = _('Error Tracking') + - if project_nav_tab? :serverless = nav_link(controller: :functions) do = link_to project_serverless_functions_path(@project), title: _('Serverless') do diff --git a/app/views/profiles/accounts/show.html.haml b/app/views/profiles/accounts/show.html.haml index e167e094240..ee2c5a13b8a 100644 --- a/app/views/profiles/accounts/show.html.haml +++ b/app/views/profiles/accounts/show.html.haml @@ -1,37 +1,37 @@ -- page_title "Account" +- page_title _('Account') - @content_class = "limit-container-width" unless fluid_layout - if current_user.ldap_user? .alert.alert-info - Some options are unavailable for LDAP accounts + = s_('Profiles|Some options are unavailable for LDAP accounts') .row.prepend-top-default .col-lg-4.profile-settings-sidebar %h4.prepend-top-0 - Two-Factor Authentication + = s_('Profiles|Two-Factor Authentication') %p - Increase your account's security by enabling Two-Factor Authentication (2FA). + = s_("Profiles|Increase your account's security by enabling Two-Factor Authentication (2FA)") .col-lg-8 %p - Status: #{current_user.two_factor_enabled? ? 'Enabled' : 'Disabled'} + #{_('Status')}: #{current_user.two_factor_enabled? ? _('Enabled') : _('Disabled')} - if current_user.two_factor_enabled? - = link_to 'Manage two-factor authentication', profile_two_factor_auth_path, class: 'btn btn-info' + = link_to _('Manage two-factor authentication'), profile_two_factor_auth_path, class: 'btn btn-info' - else .append-bottom-10 - = link_to 'Enable two-factor authentication', profile_two_factor_auth_path, class: 'btn btn-success' + = link_to _('Enable two-factor authentication'), profile_two_factor_auth_path, class: 'btn btn-success' %hr - if display_providers_on_profile? .row.prepend-top-default .col-lg-4.profile-settings-sidebar %h4.prepend-top-0 - Social sign-in + = s_('Profiles|Social sign-in') %p - Activate signin with one of the following services + = s_('Profiles|Activate signin with one of the following services') .col-lg-8 %label.label-bold - Connected Accounts - %p Click on icon to activate signin with one of the following services + = s_('Profiles|Connected Accounts') + %p= s_('Profiles|Click on icon to activate signin with one of the following services') - button_based_providers.each do |provider| .provider-btn-group .provider-btn-image @@ -39,24 +39,24 @@ - if auth_active?(provider) - if unlink_allowed?(provider) = link_to unlink_profile_account_path(provider: provider), method: :delete, class: 'provider-btn' do - Disconnect + = s_('Profiles|Disconnect') - else %a.provider-btn - Active + = s_('Profiles|Active') - else = link_to omniauth_authorize_path(:user, provider), method: :post, class: 'provider-btn not-active' do - Connect + = s_('Profiles|Connect') = render_if_exists 'profiles/accounts/group_saml_unlink_buttons', group_saml_identities: local_assigns[:group_saml_identities] %hr - if current_user.can_change_username? .row.prepend-top-default .col-lg-4.profile-settings-sidebar %h4.prepend-top-0.warning-title - Change username + = s_('Profiles|Change username') %p - Changing your username can have unintended side effects. + = s_('Profiles|Changing your username can have unintended side effects.') = succeed '.' do - = link_to 'Learn more', help_page_path('user/profile/index', anchor: 'changing-your-username'), target: '_blank' + = link_to s_('Profiles|Learn more'), help_page_path('user/profile/index', anchor: 'changing-your-username'), target: '_blank' .col-lg-8 - data = { initial_username: current_user.username, root_url: root_url, action_url: update_username_profile_path(format: :json) } #update-username{ data: data } diff --git a/app/views/projects/_export.html.haml b/app/views/projects/_export.html.haml index aa980da7e95..91deffe07c1 100644 --- a/app/views/projects/_export.html.haml +++ b/app/views/projects/_export.html.haml @@ -19,7 +19,7 @@ %ul %li Project and wiki repositories %li Project uploads - %li Project configuration including web hooks and services + %li Project configuration, including services %li Issues with comments, merge requests with diffs and comments, labels, milestones, snippets, and other project entities %li LFS objects %p @@ -28,6 +28,7 @@ %li Job traces and artifacts %li Container registry images %li CI variables + %li Webhooks %li Any encrypted tokens %p Once the exported file is ready, you will receive a notification email with a download link, or you can download it from this page. diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml index 82b2ab64a5d..e8cc3d6bcf0 100644 --- a/app/views/projects/_home_panel.html.haml +++ b/app/views/projects/_home_panel.html.haml @@ -19,11 +19,12 @@ %span.access-request-links.prepend-left-8 = render 'shared/members/access_request_links', source: @project - if @project.tag_list.present? - %span.project-tag-list.d-inline-flex.prepend-left-8.has-tooltip{ data: { container: 'body' }, title: @project.has_extra_tags? ? @project.tag_list.join(', ') : nil } + %span.project-topic-list.d-inline-flex.prepend-left-8.has-tooltip{ data: { container: 'body' }, title: @project.has_extra_topics? ? @project.tag_list.join(', ') : nil } = sprite_icon('tag', size: 16, css_class: 'icon append-right-4') - = @project.tags_to_show - - if @project.has_extra_tags? - = _("+ %{count} more") % { count: @project.count_of_extra_tags_not_shown } + = @project.topics_to_show + - if @project.has_extra_topics? + = _("+ %{count} more") % { count: @project.count_of_extra_topics_not_shown } + .project-repo-buttons.col-md-12.col-lg-6.d-inline-flex.flex-wrap.justify-content-lg-end - if current_user diff --git a/app/views/projects/artifacts/_tree_file.html.haml b/app/views/projects/artifacts/_tree_file.html.haml index cfb91568061..f42d5128715 100644 --- a/app/views/projects/artifacts/_tree_file.html.haml +++ b/app/views/projects/artifacts/_tree_file.html.haml @@ -14,4 +14,4 @@ = link_to path_to_file, class: 'str-truncated' do %span= blob.name %td - = number_to_human_size(blob.size, precision: 2) + = number_to_human_size(blob.size) diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml index 88f9b7dfc9f..4b0ea15335e 100644 --- a/app/views/projects/branches/_branch.html.haml +++ b/app/views/projects/branches/_branch.html.haml @@ -76,7 +76,7 @@ = icon("trash-o") - else = link_to project_branch_path(@project, branch.name), - class: "btn btn-remove remove-row js-ajax-loading-spinner has-tooltip", + class: "btn btn-remove remove-row qa-remove-btn js-ajax-loading-spinner has-tooltip", title: s_('Branches|Delete branch'), method: :delete, data: { confirm: s_("Branches|Deleting the '%{branch_name}' branch cannot be undone. Are you sure?") % { branch_name: branch.name } }, diff --git a/app/views/projects/branches/_panel.html.haml b/app/views/projects/branches/_panel.html.haml index 0e4b119bb54..93061452e12 100644 --- a/app/views/projects/branches/_panel.html.haml +++ b/app/views/projects/branches/_panel.html.haml @@ -10,7 +10,7 @@ .card.prepend-top-10 .card-header = panel_title - %ul.content-list.all-branches + %ul.content-list.all-branches.qa-all-branches - branches.first(overview_max_branches).each do |branch| = render "projects/branches/branch", branch: branch, merged: project.repository.merged_to_root_ref?(branch) - if branches.size > overview_max_branches diff --git a/app/views/projects/branches/index.html.haml b/app/views/projects/branches/index.html.haml index ca867961f6b..43f1cd01b67 100644 --- a/app/views/projects/branches/index.html.haml +++ b/app/views/projects/branches/index.html.haml @@ -35,7 +35,7 @@ - if can? current_user, :push_code, @project = link_to project_merged_branches_path(@project), - class: 'btn btn-inverted btn-remove has-tooltip', + class: 'btn btn-inverted btn-remove has-tooltip qa-delete-merged-branches', title: s_("Branches|Delete all branches that are merged into '%{default_branch}'") % { default_branch: @project.repository.root_ref }, method: :delete, data: { confirm: s_('Branches|Deleting the merged branches cannot be undone. Are you sure?'), diff --git a/app/views/projects/diffs/_warning.html.haml b/app/views/projects/diffs/_warning.html.haml index 2eef599cf84..2cc3d921abc 100644 --- a/app/views/projects/diffs/_warning.html.haml +++ b/app/views/projects/diffs/_warning.html.haml @@ -9,4 +9,4 @@ = link_to _("Plain diff"), merge_request_path(@merge_request, format: :diff), class: "btn btn-sm" = link_to _("Email patch"), merge_request_path(@merge_request, format: :patch), class: "btn btn-sm" %p - = _("To preserve performance only <strong>%{display_size} of ${real_size}</strong> files are displayed.").html_safe % { display_size: diff_files.size, real_size: diff_files.real_size } + = _("To preserve performance only <strong>%{display_size} of %{real_size}</strong> files are displayed.").html_safe % { display_size: diff_files.size, real_size: diff_files.real_size } diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index 1b52821af15..a58f736b5b4 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -39,9 +39,9 @@ = render_if_exists 'shared/repository_size_limit_setting', form: f, type: :project .form-group - = f.label :tag_list, "Tags", class: 'label-bold' + = f.label :tag_list, "Topics", class: 'label-bold' = f.text_field :tag_list, value: @project.tag_list.join(', '), maxlength: 2000, class: "form-control" - %p.form-text.text-muted Separate tags with commas. + %p.form-text.text-muted Separate topics with commas. %fieldset.features %h5.prepend-top-0= _("Project avatar") .form-group diff --git a/app/views/projects/environments/folder.html.haml b/app/views/projects/environments/folder.html.haml index b7e1cf85cb7..aebd176af9b 100644 --- a/app/views/projects/environments/folder.html.haml +++ b/app/views/projects/environments/folder.html.haml @@ -1,8 +1,5 @@ - @no_container = true - page_title _("Environments") -#environments-folder-list-view{ data: { endpoint: folder_project_environments_path(@project, @folder, format: :json), - "folder-name" => @folder, - "can-create-deployment" => can?(current_user, :create_deployment, @project).to_s, - "can-read-environment" => can?(current_user, :read_environment, @project).to_s, +#environments-folder-list-view{ data: { environments_data: environments_folder_list_view_data, "css-class" => container_class } } diff --git a/app/views/projects/error_tracking/index.html.haml b/app/views/projects/error_tracking/index.html.haml new file mode 100644 index 00000000000..bc02c5f0e5a --- /dev/null +++ b/app/views/projects/error_tracking/index.html.haml @@ -0,0 +1,3 @@ +- page_title _('Errors') + +#js-error_tracking{ data: error_tracking_data(@project) } diff --git a/app/views/projects/issues/_nav_btns.html.haml b/app/views/projects/issues/_nav_btns.html.haml index fd6559e37ba..329efa0cdbf 100644 --- a/app/views/projects/issues/_nav_btns.html.haml +++ b/app/views/projects/issues/_nav_btns.html.haml @@ -1,5 +1,5 @@ - show_feed_buttons = local_assigns.fetch(:show_feed_buttons, true) -- show_import_button = local_assigns.fetch(:show_import_button, true) && Feature.enabled?(:issues_import_csv) && can?(current_user, :import_issues, @project) +- show_import_button = local_assigns.fetch(:show_import_button, true) && can?(current_user, :import_issues, @project) - show_export_button = local_assigns.fetch(:show_export_button, true) .nav-controls.issues-nav-controls diff --git a/app/views/projects/jobs/index.html.haml b/app/views/projects/jobs/index.html.haml index 59592abcf6a..afea5268006 100644 --- a/app/views/projects/jobs/index.html.haml +++ b/app/views/projects/jobs/index.html.haml @@ -8,10 +8,6 @@ .nav-controls - if can?(current_user, :update_build, @project) - - if @all_builds.running_or_pending.limit(1).any? # rubocop: disable CodeReuse/ActiveRecord - = link_to 'Cancel running', cancel_all_project_jobs_path(@project), - data: { confirm: 'Are you sure?' }, class: 'btn btn-danger', method: :post - - unless @repository.gitlab_ci_yml = link_to 'Get started with Pipelines', help_page_path('ci/quick_start/README'), class: 'btn btn-info' diff --git a/app/views/projects/project_members/_groups.html.haml b/app/views/projects/project_members/_groups.html.haml index 3f05e06b0c6..b5d397e3065 100644 --- a/app/views/projects/project_members/_groups.html.haml +++ b/app/views/projects/project_members/_groups.html.haml @@ -1,7 +1,6 @@ .card.project-members-groups .card-header - Groups with access to - %strong= @project.name + = _("Groups with access to <strong>%{project_name}</strong>").html_safe % { project_name: sanitize_project_name(@project.name) } %span.badge.badge-pill= group_links.size %ul.content-list.members-list = render partial: 'shared/members/group', collection: group_links, as: :group_link diff --git a/app/views/projects/project_members/_new_project_group.html.haml b/app/views/projects/project_members/_new_project_group.html.haml index 88e68f89024..079811e4e79 100644 --- a/app/views/projects/project_members/_new_project_group.html.haml +++ b/app/views/projects/project_members/_new_project_group.html.haml @@ -10,8 +10,9 @@ = select_tag :link_group_access, options_for_select(ProjectGroupLink.access_options, ProjectGroupLink.default_access), class: "form-control select-control" = icon('chevron-down') .form-text.text-muted.append-bottom-10 - = link_to _("Read more"), help_page_path("user/permissions") - about role permissions + - permissions_docs_path = help_page_path('user/permissions') + - link_start = %q{<a href="%{url}">}.html_safe % { url: permissions_docs_path } + = _("%{link_start}Read more%{link_end} about role permissions").html_safe % { link_start: link_start, link_end: '</a>'.html_safe } .form-group = label_tag :expires_at, _('Access expiration date'), class: 'label-bold' .clearable-input 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 1de7d9c6957..0590578c3fe 100644 --- a/app/views/projects/project_members/_new_project_member.html.haml +++ b/app/views/projects/project_members/_new_project_member.html.haml @@ -2,20 +2,21 @@ .col-sm-12 = form_for @project_member, as: :project_member, url: project_project_members_path(@project), html: { class: 'users-project-form' } do |f| .form-group - = label_tag :user_ids, "Select members to invite", class: "label-bold" + = label_tag :user_ids, _("Select members to invite"), class: "label-bold" = users_select_tag(:user_ids, multiple: true, class: "input-clamp qa-member-select-input", scope: :all, email_user: true, placeholder: "Search for members to update or invite") .form-group - = label_tag :access_level, "Choose a role permission", class: "label-bold" + = label_tag :access_level, _("Choose a role permission"), class: "label-bold" .select-wrapper = select_tag :access_level, options_for_select(ProjectMember.access_level_roles, @project_member.access_level), class: "form-control project-access-select select-control" = icon('chevron-down') .form-text.text-muted.append-bottom-10 - = link_to "Read more", help_page_path("user/permissions") - about role permissions + - permissions_docs_path = help_page_path('user/permissions') + - link_start = %q{<a href="%{url}">}.html_safe % { url: permissions_docs_path } + = _("%{link_start}Read more%{link_end} about role permissions").html_safe % { link_start: link_start, link_end: '</a>'.html_safe } .form-group .clearable-input - = label_tag :expires_at, 'Access expiration date', class: 'label-bold' + = label_tag :expires_at, _('Access expiration date'), class: 'label-bold' = 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" + = 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") diff --git a/app/views/projects/project_members/_team.html.haml b/app/views/projects/project_members/_team.html.haml index 9682f8ac922..b92ecf4412f 100644 --- a/app/views/projects/project_members/_team.html.haml +++ b/app/views/projects/project_members/_team.html.haml @@ -4,14 +4,13 @@ .card .card-header.flex-project-members-panel %span.flex-project-title - Members of - %strong= project.name + = _("Members of <strong>%{project_name}</strong>").html_safe % { project_name: sanitize_project_name(project.name) } %span.badge.badge-pill= members.total_count = form_tag project_project_members_path(project), method: :get, class: 'form-inline member-search-form flex-project-members-form' do .form-group .position-relative - = search_field_tag :search, params[:search], { placeholder: 'Find existing members by name', class: 'form-control', spellcheck: false } - %button.member-search-btn{ type: "submit", "aria-label" => "Submit search" } + = search_field_tag :search, params[:search], { placeholder: _('Find existing members by name'), class: 'form-control', spellcheck: false } + %button.member-search-btn{ type: "submit", "aria-label" => _("Submit search") } = icon("search") = render 'shared/members/sort_dropdown' %ul.content-list.members-list.qa-members-list diff --git a/app/views/projects/project_members/import.html.haml b/app/views/projects/project_members/import.html.haml index 8b93e81cd31..bcca943de6a 100644 --- a/app/views/projects/project_members/import.html.haml +++ b/app/views/projects/project_members/import.html.haml @@ -1,15 +1,15 @@ -- page_title "Import members" +- page_title _("Import members") %h3.page-title - Import members from another project + = _("Import members from another project") %p.light - Only project members will be imported. Group members will be skipped. + = _("Only project members will be imported. Group members will be skipped.") %hr = form_tag apply_import_project_project_members_path(@project), method: 'post' do .form-group.row - = label_tag :source_project_id, "Project", class: 'col-form-label col-sm-2' + = label_tag :source_project_id, _("Project"), class: 'col-form-label col-sm-2' .col-sm-10= select_tag(:source_project_id, options_from_collection_for_select(@projects, :id, :name_with_namespace), prompt: "Select project", class: "select2 lg", required: true) .form-actions - = button_tag 'Import project members', class: "btn btn-success" - = link_to "Cancel", project_project_members_path(@project), class: "btn btn-cancel" + = button_tag _('Import project members'), class: "btn btn-success" + = link_to _("Cancel"), project_project_members_path(@project), class: "btn btn-cancel" diff --git a/app/views/projects/project_members/index.html.haml b/app/views/projects/project_members/index.html.haml index 14ed3345765..242ff91f539 100644 --- a/app/views/projects/project_members/index.html.haml +++ b/app/views/projects/project_members/index.html.haml @@ -1,39 +1,34 @@ -- page_title "Members" +- page_title _("Members") .row.prepend-top-default .col-lg-12 %h4 - Project members + = _("Project members") - if can?(current_user, :admin_project_member, @project) %p - You can invite a new member to - %strong= @project.name - or invite another group. + = _("You can invite a new member to <strong>%{project_name}</strong> or invite another group.").html_safe % { project_name: sanitize_project_name(@project.name) } - else %p - Members can be added by project - %i Maintainers - or - %i Owners + = _("Members can be added by project <i>Maintainers</i> or <i>Owners</i>").html_safe .light - if can?(current_user, :admin_project_member, @project) %ul.nav-links.nav.nav-tabs.gitlab-tabs{ role: 'tablist' } %li.nav-tab{ role: 'presentation' } - %a.nav-link.active{ href: '#invite-member-pane', id: 'invite-member-tab', data: { toggle: 'tab' }, role: 'tab' } Invite member + %a.nav-link.active{ href: '#invite-member-pane', id: 'invite-member-tab', data: { toggle: 'tab' }, role: 'tab' }= _("Invite member") - if @project.allowed_to_share_with_group? %li.nav-tab{ role: 'presentation' } - %a.nav-link{ href: '#invite-group-pane', id: 'invite-group-tab', data: { toggle: 'tab' }, role: 'tab' } Invite group + %a.nav-link{ href: '#invite-group-pane', id: 'invite-group-tab', data: { toggle: 'tab' }, role: 'tab' }= _("Invite group") .tab-content.gitlab-tab-content .tab-pane.active{ id: 'invite-member-pane', role: 'tabpanel' } - = render 'projects/project_members/new_project_member', tab_title: 'Invite member' + = render 'projects/project_members/new_project_member', tab_title: _('Invite member') .tab-pane{ id: 'invite-group-pane', role: 'tabpanel' } - = render 'projects/project_members/new_project_group', tab_title: 'Invite group' + = render 'projects/project_members/new_project_group', tab_title: _('Invite group') = render 'shared/members/requests', membership_source: @project, requesters: @requesters .clearfix %h5.member.existing-title - Existing members and groups + = _("Existing members and groups") - if @group_links.any? = render 'projects/project_members/groups', group_links: @group_links diff --git a/app/views/projects/runners/_group_runners.html.haml b/app/views/projects/runners/_group_runners.html.haml index a6c16c70313..a24ada53bac 100644 --- a/app/views/projects/runners/_group_runners.html.haml +++ b/app/views/projects/runners/_group_runners.html.haml @@ -32,6 +32,6 @@ - else %h4.underlined-title - = _('Available group Runners : %{runners}').html_safe % { runners: @group_runners.count } + = _('Available group Runners: %{runners}').html_safe % { runners: @group_runners.count } %ul.bordered-list = render partial: 'projects/runners/runner', collection: @group_runners, as: :runner diff --git a/app/views/projects/serverless/functions/show.html.haml b/app/views/projects/serverless/functions/show.html.haml new file mode 100644 index 00000000000..29737b7014a --- /dev/null +++ b/app/views/projects/serverless/functions/show.html.haml @@ -0,0 +1,16 @@ +- @no_container = true +- @content_class = "limit-container-width" unless fluid_layout + +- add_to_breadcrumbs('Serverless', project_serverless_functions_path(@project)) + +- page_title @service[:name] + +.serverless-function-details-page.js-serverless-function-details-page{ data: { service: @service.as_json } } +%div{ class: [container_class, ('limit-container-width' unless fluid_layout)] } + .top-area.adjust + .serverless-function-details#js-serverless-function-details + + .js-serverless-function-notice + .flash-container + + .function-holder.js-function-holder.input-group diff --git a/app/views/projects/settings/operations/_error_tracking.html.haml b/app/views/projects/settings/operations/_error_tracking.html.haml index 71335e4dfd0..871b60f05ba 100644 --- a/app/views/projects/settings/operations/_error_tracking.html.haml +++ b/app/views/projects/settings/operations/_error_tracking.html.haml @@ -18,7 +18,7 @@ = form.label :enabled, _('Active'), class: 'form-check-label' .form-group = form.label :api_url, _('Sentry API URL'), class: 'label-bold' - = form.url_field :api_url, class: 'form-control', placeholder: _('http://<sentry-host>/api/0/projects/{organization_slug}/{project_slug}/issues/') + = form.url_field :api_url, class: 'form-control', placeholder: _('http://<sentry-host>/api/0/projects/{organization_slug}/{project_slug}/') %p.form-text.text-muted = _('Enter your Sentry API URL') .form-group diff --git a/app/views/search/_category.html.haml b/app/views/search/_category.html.haml index ff9a7b53a86..aaf9b973cda 100644 --- a/app/views/search/_category.html.haml +++ b/app/views/search/_category.html.haml @@ -6,75 +6,75 @@ - if project_search_tabs?(:blobs) %li{ class: active_when(@scope == 'blobs') } = link_to search_filter_path(scope: 'blobs') do - Code + = _("Code") %span.badge.badge-pill = @search_results.blobs_count - if project_search_tabs?(:issues) %li{ class: active_when(@scope == 'issues') } = link_to search_filter_path(scope: 'issues') do - Issues + = _("Issues") %span.badge.badge-pill = limited_count(@search_results.limited_issues_count) - if project_search_tabs?(:merge_requests) %li{ class: active_when(@scope == 'merge_requests') } = link_to search_filter_path(scope: 'merge_requests') do - Merge requests + = _("Merge requests") %span.badge.badge-pill = limited_count(@search_results.limited_merge_requests_count) - if project_search_tabs?(:milestones) %li{ class: active_when(@scope == 'milestones') } = link_to search_filter_path(scope: 'milestones') do - Milestones + = _("Milestones") %span.badge.badge-pill = limited_count(@search_results.limited_milestones_count) - if project_search_tabs?(:notes) %li{ class: active_when(@scope == 'notes') } = link_to search_filter_path(scope: 'notes') do - Comments + = _("Comments") %span.badge.badge-pill = limited_count(@search_results.limited_notes_count) - if project_search_tabs?(:wiki) %li{ class: active_when(@scope == 'wiki_blobs') } = link_to search_filter_path(scope: 'wiki_blobs') do - Wiki + = _("Wiki") %span.badge.badge-pill = @search_results.wiki_blobs_count - if project_search_tabs?(:commits) %li{ class: active_when(@scope == 'commits') } = link_to search_filter_path(scope: 'commits') do - Commits + = _("Commits") %span.badge.badge-pill = @search_results.commits_count - elsif @show_snippets %li{ class: active_when(@scope == 'snippet_blobs') } = link_to search_filter_path(scope: 'snippet_blobs', snippets: true, group_id: nil, project_id: nil) do - Snippet Contents + = _("Snippet Contents") %span.badge.badge-pill = @search_results.snippet_blobs_count %li{ class: active_when(@scope == 'snippet_titles') } = link_to search_filter_path(scope: 'snippet_titles', snippets: true, group_id: nil, project_id: nil) do - Titles and Filenames + = _("Titles and Filenames") %span.badge.badge-pill = @search_results.snippet_titles_count - else %li{ class: active_when(@scope == 'projects') } = link_to search_filter_path(scope: 'projects') do - Projects + = _("Projects") %span.badge.badge-pill = limited_count(@search_results.limited_projects_count) %li{ class: active_when(@scope == 'issues') } = link_to search_filter_path(scope: 'issues') do - Issues + = _("Issues") %span.badge.badge-pill = limited_count(@search_results.limited_issues_count) %li{ class: active_when(@scope == 'merge_requests') } = link_to search_filter_path(scope: 'merge_requests') do - Merge requests + = _("Merge requests") %span.badge.badge-pill = limited_count(@search_results.limited_merge_requests_count) %li{ class: active_when(@scope == 'milestones') } = link_to search_filter_path(scope: 'milestones') do - Milestones + = _("Milestones") %span.badge.badge-pill = limited_count(@search_results.limited_milestones_count) diff --git a/app/views/search/_filter.html.haml b/app/views/search/_filter.html.haml index 6837f64f132..c8b6a3258ab 100644 --- a/app/views/search/_filter.html.haml +++ b/app/views/search/_filter.html.haml @@ -3,31 +3,31 @@ - if params[:project_id].present? = hidden_field_tag :project_id, params[:project_id] .dropdown - %button.dropdown-menu-toggle.js-search-group-dropdown{ type: "button", data: { toggle: "dropdown", default_label: "Group:", group_id: params[:group_id] } } + %button.dropdown-menu-toggle.js-search-group-dropdown{ type: "button", data: { toggle: "dropdown", default_label: _('Group:'), group_id: params[:group_id] } } %span.dropdown-toggle-text - Group: + = _("Group:") - if @group.present? = @group.name - else - Any + = _("Any") = icon("chevron-down") .dropdown-menu.dropdown-select.dropdown-menu-selectable.dropdown-menu-right - = dropdown_title("Filter results by group") - = dropdown_filter("Search groups") + = dropdown_title(_("Filter results by group")) + = dropdown_filter(_("Search groups")) = dropdown_content = dropdown_loading .dropdown.project-filter - %button.dropdown-menu-toggle.js-search-project-dropdown{ type: "button", data: { toggle: "dropdown", default_label: "Project:" } } + %button.dropdown-menu-toggle.js-search-project-dropdown{ type: "button", data: { toggle: "dropdown", default_label: _('Project:') } } %span.dropdown-toggle-text - Project: + = _("Project:") - if @project.present? = @project.full_name - else - Any + = _("Any") = icon("chevron-down") .dropdown-menu.dropdown-select.dropdown-menu-selectable.dropdown-menu-right - = dropdown_title("Filter results by project") - = dropdown_filter("Search projects") + = dropdown_title(_("Filter results by project")) + = dropdown_filter(_("Search projects")) = dropdown_content = dropdown_loading diff --git a/app/views/search/_form.html.haml b/app/views/search/_form.html.haml index a4a5cec1314..4af0c6bf84a 100644 --- a/app/views/search/_form.html.haml +++ b/app/views/search/_form.html.haml @@ -4,12 +4,12 @@ .search-holder .search-field-holder - = search_field_tag :search, params[:search], placeholder: "Search for projects, issues etc", class: "form-control search-text-input js-search-input", id: "dashboard_search", autofocus: true, spellcheck: false + = search_field_tag :search, params[:search], placeholder: _("Search for projects, issues, etc."), class: "form-control search-text-input js-search-input", id: "dashboard_search", autofocus: true, spellcheck: false = icon("search", class: "search-icon") %button.search-clear.js-search-clear{ class: ("hidden" if !params[:search].present?), type: "button", tabindex: "-1" } = icon("times-circle") %span.sr-only - Clear search + = _("Clear search") - unless params[:snippets].eql? 'true' = render 'filter' - = button_tag "Search", class: "btn btn-success btn-search" + = button_tag _("Search"), class: "btn btn-success btn-search" diff --git a/app/views/search/_results.html.haml b/app/views/search/_results.html.haml index c4d52431d6e..be7a2436d16 100644 --- a/app/views/search/_results.html.haml +++ b/app/views/search/_results.html.haml @@ -6,9 +6,11 @@ = search_entries_info(@search_objects, @scope, @search_term) - unless @show_snippets - if @project - in project #{link_to @project.full_name, [@project.namespace.becomes(Namespace), @project]} + - link_to_project = link_to(@project.full_name, [@project.namespace.becomes(Namespace), @project]) + = _("in project %{link_to_project}").html_safe % { link_to_project: link_to_project } - elsif @group - in group #{link_to @group.name, @group} + - link_to_group = link_to(@group.name, @group) + = _("in group %{link_to_group}").html_safe % { link_to_group: link_to_group } .results.prepend-top-10 - if @scope == 'commits' diff --git a/app/views/search/results/_empty.html.haml b/app/views/search/results/_empty.html.haml index 821a39d61f5..9d15995bb51 100644 --- a/app/views/search/results/_empty.html.haml +++ b/app/views/search/results/_empty.html.haml @@ -2,5 +2,5 @@ .search_glyph %h4 = icon('search') - We couldn't find any results matching + = _("We couldn't find any results matching") %code= @search_term diff --git a/app/views/search/results/_issue.html.haml b/app/views/search/results/_issue.html.haml index c413c3d4abb..796782035f2 100644 --- a/app/views/search/results/_issue.html.haml +++ b/app/views/search/results/_issue.html.haml @@ -4,7 +4,7 @@ = link_to [issue.project.namespace.becomes(Namespace), issue.project, issue] do %span.term.str-truncated= issue.title - if issue.closed? - %span.badge.badge-danger.prepend-left-5 Closed + %span.badge.badge-danger.prepend-left-5= _("Closed") .float-right ##{issue.iid} - if issue.description.present? .description.term diff --git a/app/views/search/results/_merge_request.html.haml b/app/views/search/results/_merge_request.html.haml index 519176af108..f0e0af11f27 100644 --- a/app/views/search/results/_merge_request.html.haml +++ b/app/views/search/results/_merge_request.html.haml @@ -3,9 +3,9 @@ = link_to [merge_request.target_project.namespace.becomes(Namespace), merge_request.target_project, merge_request] do %span.term.str-truncated= merge_request.title - if merge_request.merged? - %span.badge.badge-primary.prepend-left-5 Merged + %span.badge.badge-primary.prepend-left-5= _("Merged") - elsif merge_request.closed? - %span.badge.badge-danger.prepend-left-5 Closed + %span.badge.badge-danger.prepend-left-5= _("Closed") .float-right= merge_request.to_reference - if merge_request.description.present? .description.term diff --git a/app/views/search/results/_note.html.haml b/app/views/search/results/_note.html.haml index e4ab7b0541f..6717939d034 100644 --- a/app/views/search/results/_note.html.haml +++ b/app/views/search/results/_note.html.haml @@ -6,14 +6,14 @@ %h5.note-search-caption.str-truncated %i.fa.fa-comment = link_to_member(project, note.author, avatar: false) - commented on - = link_to project.full_name, project + - link_to_project = link_to(project.full_name, project) + = _("commented on %{link_to_project}").html_safe % { link_to_project: link_to_project } · - if note.for_commit? - = link_to_if(noteable_identifier, "Commit #{truncate_sha(note.commit_id)}", note_url) do + = link_to_if(noteable_identifier, _("Commit %{commit_id}") % { commit_id: truncate_sha(note.commit_id) }, note_url) do = truncate_sha(note.commit_id) - %span.light Commit deleted + %span.light= _("Commit deleted") - else %span #{note.noteable_type.titleize} ##{noteable_identifier} diff --git a/app/views/search/results/_snippet_blob.html.haml b/app/views/search/results/_snippet_blob.html.haml index b4ecd7bbae9..e0130f9a4b5 100644 --- a/app/views/search/results/_snippet_blob.html.haml +++ b/app/views/search/results/_snippet_blob.html.haml @@ -24,7 +24,7 @@ = markup(snippet.file_name, chunk[:data], legacy_render_context(params)) - else .file-content.code - .nothing-here-block Empty file + .nothing-here-block= _("Empty file") - else .file-content.code.js-syntax-highlight .line-numbers @@ -42,4 +42,4 @@ = highlight(snippet.file_name, chunk[:data]) - else .file-content.code - .nothing-here-block Empty file + .nothing-here-block= _("Empty file") diff --git a/app/views/search/results/_snippet_title.html.haml b/app/views/search/results/_snippet_title.html.haml index 9c8afb2165b..1e01088d9e6 100644 --- a/app/views/search/results/_snippet_title.html.haml +++ b/app/views/search/results/_snippet_title.html.haml @@ -5,7 +5,7 @@ - if snippet_title.private? %span.badge.badge-gray %i.fa.fa-lock - private + = _("private") %span.cgray.monospace.tiny.float-right.term = snippet_title.file_name diff --git a/app/views/search/show.html.haml b/app/views/search/show.html.haml index 499697f2777..3260d05f509 100644 --- a/app/views/search/show.html.haml +++ b/app/views/search/show.html.haml @@ -1,5 +1,5 @@ - @hide_top_links = true -- breadcrumb_title "Search" +- breadcrumb_title _("Search") - page_title @search_term .prepend-top-default diff --git a/app/views/shared/_mini_pipeline_graph.html.haml b/app/views/shared/_mini_pipeline_graph.html.haml index 28b34e38b15..8607f87ce0b 100644 --- a/app/views/shared/_mini_pipeline_graph.html.haml +++ b/app/views/shared/_mini_pipeline_graph.html.haml @@ -7,7 +7,6 @@ .stage-container.dropdown{ class: klass } %button.mini-pipeline-graph-dropdown-toggle.has-tooltip.js-builds-dropdown-button{ class: "ci-status-icon-#{detailed_status.group}", type: 'button', data: { toggle: 'dropdown', title: "#{stage.name}: #{detailed_status.label}", placement: 'top', "stage-endpoint" => stage_ajax_project_pipeline_path(pipeline.project, pipeline, stage: stage.name) } } = sprite_icon(icon_status) - = icon('caret-down') %ul.dropdown-menu.mini-pipeline-graph-dropdown-menu.js-builds-dropdown-container %li.js-builds-dropdown-list.scrollable-menu diff --git a/app/views/shared/_personal_access_tokens_created_container.html.haml b/app/views/shared/_personal_access_tokens_created_container.html.haml index 3150d39b84a..a8d3de66418 100644 --- a/app/views/shared/_personal_access_tokens_created_container.html.haml +++ b/app/views/shared/_personal_access_tokens_created_container.html.haml @@ -6,7 +6,7 @@ = container_title .form-group .input-group - = text_field_tag 'created-personal-access-token', new_token_value, readonly: true, class: "form-control js-select-on-focus", 'aria-describedby' => "created-token-help-block" + = text_field_tag 'created-personal-access-token', new_token_value, readonly: true, class: "qa-created-personal-access-token form-control js-select-on-focus", 'aria-describedby' => "created-token-help-block" %span.input-group-append = clipboard_button(text: new_token_value, title: clipboard_button_title, placement: "left", class: "input-group-text btn-default btn-clipboard") %span#created-token-help-block.form-text.text-muted.text-danger Make sure you save it - you won't be able to access it again. diff --git a/app/views/shared/_personal_access_tokens_form.html.haml b/app/views/shared/_personal_access_tokens_form.html.haml index f4df7bdcd83..0891b3459ec 100644 --- a/app/views/shared/_personal_access_tokens_form.html.haml +++ b/app/views/shared/_personal_access_tokens_form.html.haml @@ -12,7 +12,7 @@ .row .form-group.col-md-6 = f.label :name, class: 'label-bold' - = f.text_field :name, class: "form-control", required: true + = f.text_field :name, class: "form-control qa-personal-access-token-name-field", required: true .row .form-group.col-md-6 @@ -26,4 +26,4 @@ = render 'shared/tokens/scopes_form', prefix: 'personal_access_token', token: token, scopes: scopes .prepend-top-default - = f.submit "Create #{type} token", class: "btn btn-success" + = f.submit "Create #{type} token", class: "btn btn-success qa-create-token-button" diff --git a/app/views/shared/_personal_access_tokens_table.html.haml b/app/views/shared/_personal_access_tokens_table.html.haml index 2efd03d4867..49f3aae0f98 100644 --- a/app/views/shared/_personal_access_tokens_table.html.haml +++ b/app/views/shared/_personal_access_tokens_table.html.haml @@ -29,7 +29,7 @@ %span.token-never-expires-label Never %td= token.scopes.present? ? token.scopes.join(", ") : "<no scopes selected>" - path = impersonation ? revoke_admin_user_impersonation_token_path(token.user, token) : revoke_profile_personal_access_token_path(token) - %td= link_to "Revoke", path, method: :put, class: "btn btn-danger float-right", data: { confirm: "Are you sure you want to revoke this #{type} Token? This action cannot be undone." } + %td= link_to "Revoke", path, method: :put, class: "btn btn-danger float-right qa-revoke-button", data: { confirm: "Are you sure you want to revoke this #{type} Token? This action cannot be undone." } - else .settings-message.text-center This user has no active #{type} Tokens. diff --git a/app/views/shared/empty_states/_issues.html.haml b/app/views/shared/empty_states/_issues.html.haml index 0434860dec4..9173b802dd4 100644 --- a/app/views/shared/empty_states/_issues.html.haml +++ b/app/views/shared/empty_states/_issues.html.haml @@ -1,7 +1,11 @@ - button_path = local_assigns.fetch(:button_path, false) - project_select_button = local_assigns.fetch(:project_select_button, false) -- show_import_button = local_assigns.fetch(:show_import_button, false) && Feature.enabled?(:issues_import_csv) && can?(current_user, :import_issues, @project) +- show_import_button = local_assigns.fetch(:show_import_button, false) && can?(current_user, :import_issues, @project) - has_button = button_path || project_select_button +- closed_issues_count = issuables_count_for_state(:issues, :closed) +- opened_issues_count = issuables_count_for_state(:issues, :opened) +- is_opened_state = params[:state] == 'opened' +- is_closed_state = params[:state] == 'closed' .row.empty-state .col-12 @@ -14,6 +18,20 @@ = _("Sorry, your filter produced no results") %p.text-center = _("To widen your search, change or remove filters above") + - if show_new_issue_link?(@project) + .text-center + = link_to _("New issue"), new_project_issue_path(@project), class: "btn btn-success", title: _("New issue"), id: "new_issue_body_link" + - elsif is_opened_state && opened_issues_count == 0 && closed_issues_count > 0 + %h4.text-center + = _("There are no open issues") + %p.text-center + = _("To keep this project going, create a new issue") + - if show_new_issue_link?(@project) + .text-center + = link_to _("New issue"), new_project_issue_path(@project), class: "btn btn-success", title: _("New issue"), id: "new_issue_body_link" + - elsif is_closed_state && opened_issues_count > 0 && closed_issues_count == 0 + %h4.text-center + = _("There are no closed issues") - elsif current_user %h4 = _("The Issue Tracker is the place to add things that need to be improved or solved in a project") diff --git a/app/views/shared/empty_states/_merge_requests.html.haml b/app/views/shared/empty_states/_merge_requests.html.haml index 06ceb9738bc..be5b1c6b6ce 100644 --- a/app/views/shared/empty_states/_merge_requests.html.haml +++ b/app/views/shared/empty_states/_merge_requests.html.haml @@ -1,6 +1,11 @@ - button_path = local_assigns.fetch(:button_path, false) - project_select_button = local_assigns.fetch(:project_select_button, false) - has_button = button_path || project_select_button +- closed_merged_count = issuables_count_for_state(:merged, :closed) +- opened_merged_count = issuables_count_for_state(:merged, :opened) +- is_opened_state = params[:state] == 'opened' +- is_closed_state = params[:state] == 'closed' +- can_create_merge_request = merge_request_source_project_for_project(@project) .row.empty-state.merge-requests .col-12 @@ -13,6 +18,20 @@ = _("Sorry, your filter produced no results") %p.text-center = _("To widen your search, change or remove filters above") + .text-center + - if can_create_merge_request + = link_to _("New merge request"), project_new_merge_request_path(@project), class: "btn btn-success", title: _("New merge request"), id: "new_merge_request_body_link" + - elsif is_opened_state && opened_merged_count == 0 && closed_merged_count > 0 + %h4.text-center + = _("There are no open merge requests") + %p.text-center + = _("To keep this project going, create a new merge request") + .text-center + - if can_create_merge_request + = link_to _("New merge request"), project_new_merge_request_path(@project), class: "btn btn-success", title: _("New merge request"), id: "new_merge_request_body_link" + - elsif is_closed_state && opened_merged_count > 0 && closed_merged_count == 0 + %h4.text-center + = _("There are no closed merge requests") - else %h4 = _("Merge requests are a place to propose changes you've made to a project and discuss those changes with others") diff --git a/app/views/shared/issuable/_board_create_list_dropdown.html.haml b/app/views/shared/issuable/_board_create_list_dropdown.html.haml index 4597d9439fa..fd413bd68c8 100644 --- a/app/views/shared/issuable/_board_create_list_dropdown.html.haml +++ b/app/views/shared/issuable/_board_create_list_dropdown.html.haml @@ -1,7 +1,7 @@ .dropdown.prepend-left-10#js-add-list %button.btn.btn-success.btn-inverted.js-new-board-list{ type: "button", data: board_list_data } Add list - .dropdown-menu.dropdown-menu-paging.dropdown-menu-right.dropdown-menu-issues-board-new.dropdown-menu-selectable.js-tab-container-labels + .dropdown-menu.dropdown-extended-height.dropdown-menu-paging.dropdown-menu-right.dropdown-menu-issues-board-new.dropdown-menu-selectable.js-tab-container-labels = render partial: "shared/issuable/label_page_default", locals: { show_footer: true, show_create: true, show_boards_content: true, title: "Add list" } - if can?(current_user, :admin_label, board.parent) = render partial: "shared/issuable/label_page_create" diff --git a/app/views/shared/issuable/_sidebar_assignees.html.haml b/app/views/shared/issuable/_sidebar_assignees.html.haml index c5cce1823f0..1a59055f652 100644 --- a/app/views/shared/issuable/_sidebar_assignees.html.haml +++ b/app/views/shared/issuable/_sidebar_assignees.html.haml @@ -25,7 +25,7 @@ .value.hide-collapsed - if issuable_sidebar[:assignee] = link_to_member(@project, assignee, size: 32, extra_class: 'bold') do - - if issuable_sidebar[:assignee][:can_merge] + - unless issuable_sidebar[:assignee][:can_merge] %span.float-right.cannot-be-merged{ data: { toggle: 'tooltip', placement: 'left' }, title: _('Not allowed to merge') } = icon('exclamation-triangle', 'aria-hidden': 'true') %span.username diff --git a/app/views/shared/notes/_comment_button.html.haml b/app/views/shared/notes/_comment_button.html.haml index f487c0bf0d5..c3f5eeb0da6 100644 --- a/app/views/shared/notes/_comment_button.html.haml +++ b/app/views/shared/notes/_comment_button.html.haml @@ -1,10 +1,10 @@ - noteable_name = @note.noteable.human_class_name .float-left.btn-group.append-right-10.droplab-dropdown.comment-type-dropdown.js-comment-type-dropdown - %input.btn.btn-nr.btn-success.comment-btn.js-comment-button.js-comment-submit-button{ type: 'submit', value: _('Comment') } + %input.btn.btn-nr.btn-success.js-comment-button.js-comment-submit-button{ type: 'submit', value: _('Comment') } - if @note.can_be_discussion_note? - = button_tag type: 'button', class: 'btn btn-nr dropdown-toggle comment-btn js-note-new-discussion js-disable-on-submit', data: { 'dropdown-trigger' => '#resolvable-comment-menu' }, 'aria-label' => _('Open comment type dropdown') do + = button_tag type: 'button', class: 'btn btn-nr dropdown-toggle btn-success js-note-new-discussion js-disable-on-submit', data: { 'dropdown-trigger' => '#resolvable-comment-menu' }, 'aria-label' => _('Open comment type dropdown') do = icon('caret-down', class: 'toggle-icon') %ul#resolvable-comment-menu.dropdown-menu.dropdown-open-top{ data: { dropdown: true } } diff --git a/app/views/shared/projects/_list.html.haml b/app/views/shared/projects/_list.html.haml index 15c29e14cc0..7d90d9ca4a5 100644 --- a/app/views/shared/projects/_list.html.haml +++ b/app/views/shared/projects/_list.html.haml @@ -33,4 +33,9 @@ %span you have no access to. = paginate_collection(projects, remote: remote) unless skip_pagination - else - .nothing-here-block No projects found + .nothing-here-block + .svg-content.svg-130 + = image_tag 'illustrations/profile-page/personal-project.svg' + %div + %span + = s_('UserProfile|This user doesn\'t have any personal projects') diff --git a/app/views/shared/tokens/_scopes_form.html.haml b/app/views/shared/tokens/_scopes_form.html.haml index af9db5f59a8..a5d3e1c8de0 100644 --- a/app/views/shared/tokens/_scopes_form.html.haml +++ b/app/views/shared/tokens/_scopes_form.html.haml @@ -4,6 +4,6 @@ - scopes.each do |scope| %fieldset.form-group.form-check - = check_box_tag "#{prefix}[scopes][]", scope, token.scopes.include?(scope), id: "#{prefix}_scopes_#{scope}", class: 'form-check-input' + = check_box_tag "#{prefix}[scopes][]", scope, token.scopes.include?(scope), id: "#{prefix}_scopes_#{scope}", class: "form-check-input qa-#{scope}-radio" = label_tag ("#{prefix}_scopes_#{scope}"), scope, class: 'label-bold form-check-label' .text-secondary= t scope, scope: [:doorkeeper, :scope_desc] diff --git a/app/workers/git_garbage_collect_worker.rb b/app/workers/git_garbage_collect_worker.rb index d3628b23189..b33e9b1f718 100644 --- a/app/workers/git_garbage_collect_worker.rb +++ b/app/workers/git_garbage_collect_worker.rb @@ -23,6 +23,7 @@ class GitGarbageCollectWorker end task = task.to_sym + project.link_pool_repository gitaly_call(task, project.repository.raw_repository) # Refresh the branch cache in case garbage collection caused a ref lookup to fail diff --git a/app/workers/object_pool/join_worker.rb b/app/workers/object_pool/join_worker.rb index 07676011b2a..9c5161fd55a 100644 --- a/app/workers/object_pool/join_worker.rb +++ b/app/workers/object_pool/join_worker.rb @@ -5,14 +5,13 @@ module ObjectPool include ApplicationWorker include ObjectPoolQueue - def perform(pool_id, project_id) - pool = PoolRepository.find_by_id(pool_id) - return unless pool&.joinable? - + # The use of pool id is deprecated. Keeping the argument allows old jobs to + # still be performed. + def perform(_pool_id, project_id) project = Project.find_by_id(project_id) - return unless project + return unless project&.pool_repository&.joinable? - pool.link_repository(project.repository) + project.link_pool_repository Projects::HousekeepingService.new(project).execute end diff --git a/app/workers/reactive_caching_worker.rb b/app/workers/reactive_caching_worker.rb index 96ff8cd6222..7c66ac046ea 100644 --- a/app/workers/reactive_caching_worker.rb +++ b/app/workers/reactive_caching_worker.rb @@ -12,7 +12,7 @@ class ReactiveCachingWorker end return unless klass - klass.find_by(id: id).try(:exclusively_update_reactive_cache!, *args) + klass.find_by(klass.primary_key => id).try(:exclusively_update_reactive_cache!, *args) end # rubocop: enable CodeReuse/ActiveRecord end diff --git a/app/workers/remote_mirror_notification_worker.rb b/app/workers/remote_mirror_notification_worker.rb index 70c2e857d09..5bafe8e2046 100644 --- a/app/workers/remote_mirror_notification_worker.rb +++ b/app/workers/remote_mirror_notification_worker.rb @@ -9,7 +9,10 @@ class RemoteMirrorNotificationWorker # We check again if there's an error because a newer run since this job was # fired could've completed successfully. return unless remote_mirror && remote_mirror.last_error.present? + return if remote_mirror.error_notification_sent? NotificationService.new.remote_mirror_update_failed(remote_mirror) + + remote_mirror.after_sent_notification end end diff --git a/bin/changelog b/bin/changelog index 758c036161e..328d9495b96 100755 --- a/bin/changelog +++ b/bin/changelog @@ -148,7 +148,7 @@ class ChangelogEntry def execute assert_feature_branch! - assert_title! + assert_title! unless editor assert_new_file! # Read type from $stdin unless is already set @@ -162,6 +162,10 @@ class ChangelogEntry write amend_commit if options.amend end + + if editor + system("#{editor} '#{file_path}'") + end end private @@ -180,6 +184,10 @@ class ChangelogEntry File.write(file_path, contents) end + def editor + ENV['EDITOR'] + end + def amend_commit fail_with "git add failed" unless system(*%W[git add #{file_path}]) diff --git a/changelogs/unreleased/25043-empty-states.yml b/changelogs/unreleased/25043-empty-states.yml new file mode 100644 index 00000000000..529a8b3206f --- /dev/null +++ b/changelogs/unreleased/25043-empty-states.yml @@ -0,0 +1,5 @@ +--- +title: Make issuable empty states actionable +merge_request: 24077 +author: +type: changed diff --git a/changelogs/unreleased/25341-add-what-s-new-menu-item-in-top-navigation.yml b/changelogs/unreleased/25341-add-what-s-new-menu-item-in-top-navigation.yml new file mode 100644 index 00000000000..da1777827cb --- /dev/null +++ b/changelogs/unreleased/25341-add-what-s-new-menu-item-in-top-navigation.yml @@ -0,0 +1,5 @@ +--- +title: Resolve Add What's new menu item in top navigation +merge_request: 23186 +author: +type: added diff --git a/changelogs/unreleased/37990-task-list-bracket.yml b/changelogs/unreleased/37990-task-list-bracket.yml new file mode 100644 index 00000000000..ffa77cf0af7 --- /dev/null +++ b/changelogs/unreleased/37990-task-list-bracket.yml @@ -0,0 +1,5 @@ +--- +title: Fix ambiguous brackets in task lists +merge_request: 18514 +author: Jared Deckard <jared.deckard@gmail.com> +type: fixed diff --git a/changelogs/unreleased/42769-remove-expansion-hover-animation-from-status-icon-buttons.yml b/changelogs/unreleased/42769-remove-expansion-hover-animation-from-status-icon-buttons.yml new file mode 100644 index 00000000000..5a4ff8b3358 --- /dev/null +++ b/changelogs/unreleased/42769-remove-expansion-hover-animation-from-status-icon-buttons.yml @@ -0,0 +1,5 @@ +--- +title: Remove expansion hover animation from pipeline status icon buttons +merge_request: 24268 +author: Nathan Friend +type: changed diff --git a/changelogs/unreleased/45779-fix-default-visibility-level-for-projects.yml b/changelogs/unreleased/45779-fix-default-visibility-level-for-projects.yml new file mode 100644 index 00000000000..b4cba5041d1 --- /dev/null +++ b/changelogs/unreleased/45779-fix-default-visibility-level-for-projects.yml @@ -0,0 +1,5 @@ +--- +title: Fix default visibility_level for new projects +merge_request: 24120 +author: Fabian Schneider @fabsrc +type: fixed diff --git a/changelogs/unreleased/47988-improve-milestone-queries-with-subqueries.yml b/changelogs/unreleased/47988-improve-milestone-queries-with-subqueries.yml new file mode 100644 index 00000000000..d1a80ab43cf --- /dev/null +++ b/changelogs/unreleased/47988-improve-milestone-queries-with-subqueries.yml @@ -0,0 +1,5 @@ +--- +title: Improve milestone queries using subqueries instead of separate queries for ids +merge_request: 24325 +author: +type: performance diff --git a/changelogs/unreleased/52275-fix-master-to-be-hyperlink.yml b/changelogs/unreleased/52275-fix-master-to-be-hyperlink.yml new file mode 100644 index 00000000000..c1cde0ceff6 --- /dev/null +++ b/changelogs/unreleased/52275-fix-master-to-be-hyperlink.yml @@ -0,0 +1,5 @@ +--- +title: Resolve In Merge Request diff screen, master is not a hyperlink +merge_request: 23874 +author: +type: fixed diff --git a/changelogs/unreleased/52278-squash-checkbox-fix.yml b/changelogs/unreleased/52278-squash-checkbox-fix.yml new file mode 100644 index 00000000000..c81748ae419 --- /dev/null +++ b/changelogs/unreleased/52278-squash-checkbox-fix.yml @@ -0,0 +1,5 @@ +--- +title: Resolve When merging an MR, the squash checkbox isnt always supported +merge_request: 24296 +author: +type: fixed diff --git a/changelogs/unreleased/52363-modifies-environment-scope-field-on-cluster-page.yml b/changelogs/unreleased/52363-modifies-environment-scope-field-on-cluster-page.yml new file mode 100644 index 00000000000..07cb35e6529 --- /dev/null +++ b/changelogs/unreleased/52363-modifies-environment-scope-field-on-cluster-page.yml @@ -0,0 +1,5 @@ +--- +title: Modifies environment scope UI on cluster page +merge_request: 24376 +author: +type: other diff --git a/changelogs/unreleased/53431-fix-upcoming-milestone-filter-for-groups.yml b/changelogs/unreleased/53431-fix-upcoming-milestone-filter-for-groups.yml new file mode 100644 index 00000000000..1e9c7f3913c --- /dev/null +++ b/changelogs/unreleased/53431-fix-upcoming-milestone-filter-for-groups.yml @@ -0,0 +1,5 @@ +--- +title: Fix upcoming milestones filter not including group milestones +merge_request: 23098 +author: Heinrich Lee Yu +type: fixed diff --git a/changelogs/unreleased/54167-rename-project-tags-to-project-topics.yml b/changelogs/unreleased/54167-rename-project-tags-to-project-topics.yml new file mode 100644 index 00000000000..6fc8aa1a195 --- /dev/null +++ b/changelogs/unreleased/54167-rename-project-tags-to-project-topics.yml @@ -0,0 +1,5 @@ +--- +title: Rename project tags to project topics +merge_request: 24219 +author: +type: other diff --git a/changelogs/unreleased/54484-anchor-links-to-comments-or-system-notes-can-break-with-discussion-filters.yml b/changelogs/unreleased/54484-anchor-links-to-comments-or-system-notes-can-break-with-discussion-filters.yml new file mode 100644 index 00000000000..4d543db567d --- /dev/null +++ b/changelogs/unreleased/54484-anchor-links-to-comments-or-system-notes-can-break-with-discussion-filters.yml @@ -0,0 +1,5 @@ +--- +title: Ensured links to a comment or system note anchor resolves to the right note if a user has a discussion filter. +merge_request: 24228 +author: +type: changed diff --git a/changelogs/unreleased/55111-gitlab-api-does-not-manage-default_branch_protection-3-value.yml b/changelogs/unreleased/55111-gitlab-api-does-not-manage-default_branch_protection-3-value.yml new file mode 100644 index 00000000000..b609fc2d60b --- /dev/null +++ b/changelogs/unreleased/55111-gitlab-api-does-not-manage-default_branch_protection-3-value.yml @@ -0,0 +1,5 @@ +--- +title: 'API: Fix default_branch_protection admin setting' +merge_request: 24398 +author: Robert Schilling +type: fixed diff --git a/changelogs/unreleased/55242-skeleton-loading-releases.yml b/changelogs/unreleased/55242-skeleton-loading-releases.yml new file mode 100644 index 00000000000..43cda64ce04 --- /dev/null +++ b/changelogs/unreleased/55242-skeleton-loading-releases.yml @@ -0,0 +1,5 @@ +--- +title: Adds skeleton loading to releases page +merge_request: +author: +type: changed diff --git a/changelogs/unreleased/55495-teamcity-use-revision-in-query.yml b/changelogs/unreleased/55495-teamcity-use-revision-in-query.yml new file mode 100644 index 00000000000..724de733b7c --- /dev/null +++ b/changelogs/unreleased/55495-teamcity-use-revision-in-query.yml @@ -0,0 +1,5 @@ +--- +title: Build number does not need to be tweaked anymore for the TeamCity integration to work properly. +merge_request: 23898 +author: +type: changed diff --git a/changelogs/unreleased/55628-artifacts-from-a-job-defined-after-a-parallel-job-are-not-downloaded.yml b/changelogs/unreleased/55628-artifacts-from-a-job-defined-after-a-parallel-job-are-not-downloaded.yml new file mode 100644 index 00000000000..071036cd568 --- /dev/null +++ b/changelogs/unreleased/55628-artifacts-from-a-job-defined-after-a-parallel-job-are-not-downloaded.yml @@ -0,0 +1,5 @@ +--- +title: Handle regular job dependencies next to parallelized job dependencies. +merge_request: 24273 +author: +type: fixed diff --git a/changelogs/unreleased/55884-adjust-emoji-and-cancel-buttons-height-in-user-status-modal-when-emoji-is-changed.yml b/changelogs/unreleased/55884-adjust-emoji-and-cancel-buttons-height-in-user-status-modal-when-emoji-is-changed.yml new file mode 100644 index 00000000000..2fbf334f5e9 --- /dev/null +++ b/changelogs/unreleased/55884-adjust-emoji-and-cancel-buttons-height-in-user-status-modal-when-emoji-is-changed.yml @@ -0,0 +1,5 @@ +--- +title: Emoji and cancel button are taller than input in set user status modal +merge_request: 24173 +author: Dhiraj Bodicherla +type: fixed diff --git a/changelogs/unreleased/55945-suggested-change-preview-highlight.yml b/changelogs/unreleased/55945-suggested-change-preview-highlight.yml new file mode 100644 index 00000000000..997290a5d50 --- /dev/null +++ b/changelogs/unreleased/55945-suggested-change-preview-highlight.yml @@ -0,0 +1,5 @@ +--- +title: Fix syntax highlighting for suggested changes preview +merge_request: 24358 +author: +type: fixed diff --git a/changelogs/unreleased/55958-inconsistent-spacing-between-note-and-user-avatar-in-discussions.yml b/changelogs/unreleased/55958-inconsistent-spacing-between-note-and-user-avatar-in-discussions.yml new file mode 100644 index 00000000000..765398cda84 --- /dev/null +++ b/changelogs/unreleased/55958-inconsistent-spacing-between-note-and-user-avatar-in-discussions.yml @@ -0,0 +1,5 @@ +--- +title: Fix spacing on discussions +merge_request: !24197 +author: +type: fixed diff --git a/changelogs/unreleased/56036-fix-translation-of-in-in-job-details-sidebar.yml b/changelogs/unreleased/56036-fix-translation-of-in-in-job-details-sidebar.yml new file mode 100644 index 00000000000..ff9d4f2c175 --- /dev/null +++ b/changelogs/unreleased/56036-fix-translation-of-in-in-job-details-sidebar.yml @@ -0,0 +1,4 @@ +title: Remove multilingual translation from the word "in" in the job details sidebar. +merge_request: 24192 +author: Nathan Friend +type: changed diff --git a/changelogs/unreleased/56110-cluster-kubernetes-api-500-error-on-post-request.yml b/changelogs/unreleased/56110-cluster-kubernetes-api-500-error-on-post-request.yml new file mode 100644 index 00000000000..4da14114225 --- /dev/null +++ b/changelogs/unreleased/56110-cluster-kubernetes-api-500-error-on-post-request.yml @@ -0,0 +1,5 @@ +--- +title: Improves restriction of multiple Kubernetes clusters through API +merge_request: 24251 +author: +type: fixed diff --git a/changelogs/unreleased/56172-docs-fix-add-include-to-ci-param-list.yml b/changelogs/unreleased/56172-docs-fix-add-include-to-ci-param-list.yml new file mode 100644 index 00000000000..92592290ac4 --- /dev/null +++ b/changelogs/unreleased/56172-docs-fix-add-include-to-ci-param-list.yml @@ -0,0 +1,5 @@ +--- +title: Update CI YAML param table with include +merge_request: !24309 +author: +type: fixed diff --git a/changelogs/unreleased/56363-inconsitent-file-size-indication-across-different-ci-pages.yml b/changelogs/unreleased/56363-inconsitent-file-size-indication-across-different-ci-pages.yml new file mode 100644 index 00000000000..7c923422534 --- /dev/null +++ b/changelogs/unreleased/56363-inconsitent-file-size-indication-across-different-ci-pages.yml @@ -0,0 +1,6 @@ +--- +title: Show CI artifact file size with 3 significant digits on 'browse job artifacts' + page +merge_request: 24387 +author: +type: fixed diff --git a/changelogs/unreleased/8688-recursive-pipelines-ce-backport.yml b/changelogs/unreleased/8688-recursive-pipelines-ce-backport.yml new file mode 100644 index 00000000000..cd7b56a1e05 --- /dev/null +++ b/changelogs/unreleased/8688-recursive-pipelines-ce-backport.yml @@ -0,0 +1,5 @@ +--- +title: Creates mixin to reduce code duplication between CE and EE in graph component +merge_request: +author: +type: other diff --git a/changelogs/unreleased/ac-pages-subgroups.yml b/changelogs/unreleased/ac-pages-subgroups.yml new file mode 100644 index 00000000000..ef5a0c1872e --- /dev/null +++ b/changelogs/unreleased/ac-pages-subgroups.yml @@ -0,0 +1,5 @@ +--- +title: Pages for subgroups +merge_request: 23505 +author: +type: added diff --git a/changelogs/unreleased/actioncontroller-parameters-deprecations.yml b/changelogs/unreleased/actioncontroller-parameters-deprecations.yml new file mode 100644 index 00000000000..ddd15c37542 --- /dev/null +++ b/changelogs/unreleased/actioncontroller-parameters-deprecations.yml @@ -0,0 +1,5 @@ +--- +title: Fix several ActionController::Parameters deprecations +merge_request: 24332 +author: Jasper Maes +type: other diff --git a/changelogs/unreleased/add-badge-count-to-projects-and-groups.yml b/changelogs/unreleased/add-badge-count-to-projects-and-groups.yml new file mode 100644 index 00000000000..e200bbaa806 --- /dev/null +++ b/changelogs/unreleased/add-badge-count-to-projects-and-groups.yml @@ -0,0 +1,5 @@ +--- +title: Add badge count to projects +merge_request: 18425 +author: George Tsiolis +type: added diff --git a/changelogs/unreleased/add-uniqueness-validation-to-url-column-in-releases-link-model.yml b/changelogs/unreleased/add-uniqueness-validation-to-url-column-in-releases-link-model.yml new file mode 100644 index 00000000000..7d767e220f7 --- /dev/null +++ b/changelogs/unreleased/add-uniqueness-validation-to-url-column-in-releases-link-model.yml @@ -0,0 +1,5 @@ +--- +title: Add uniqueness validation to url column in Releases::Link model +merge_request: 24223 +author: +type: other diff --git a/changelogs/unreleased/an-gilab-process-name.yml b/changelogs/unreleased/an-gilab-process-name.yml new file mode 100644 index 00000000000..72d811ee21f --- /dev/null +++ b/changelogs/unreleased/an-gilab-process-name.yml @@ -0,0 +1,5 @@ +--- +title: Extract process_name from GitLab::Sentry +merge_request: 24422 +author: +type: other diff --git a/changelogs/unreleased/an-opentracing-factory.yml b/changelogs/unreleased/an-opentracing-factory.yml new file mode 100644 index 00000000000..c04736f3e63 --- /dev/null +++ b/changelogs/unreleased/an-opentracing-factory.yml @@ -0,0 +1,5 @@ +--- +title: Conditionally initialize the global opentracing tracer +merge_request: 24186 +author: +type: other diff --git a/changelogs/unreleased/api-wiki-dot-slug.yml b/changelogs/unreleased/api-wiki-dot-slug.yml new file mode 100644 index 00000000000..82c76fa7450 --- /dev/null +++ b/changelogs/unreleased/api-wiki-dot-slug.yml @@ -0,0 +1,5 @@ +--- +title: 'API: Support dots in wiki slugs' +merge_request: 24383 +author: Robert Schilling +type: fixed diff --git a/changelogs/unreleased/auto-devops-custom-domains.yml b/changelogs/unreleased/auto-devops-custom-domains.yml new file mode 100644 index 00000000000..37e8ee26a4d --- /dev/null +++ b/changelogs/unreleased/auto-devops-custom-domains.yml @@ -0,0 +1,5 @@ +--- +title: Added support for custom hosts/domains to Auto DevOps +merge_request: 24248 +author: walkafwalka +type: added diff --git a/changelogs/unreleased/auto-devops-kubectl-1-11-6.yml b/changelogs/unreleased/auto-devops-kubectl-1-11-6.yml new file mode 100644 index 00000000000..1a8cdead4ac --- /dev/null +++ b/changelogs/unreleased/auto-devops-kubectl-1-11-6.yml @@ -0,0 +1,5 @@ +--- +title: Bump kubectl in Auto DevOps to 1.11.6 +merge_request: 24176 +author: +type: other diff --git a/changelogs/unreleased/backup_aws_sse-c.yml b/changelogs/unreleased/backup_aws_sse-c.yml new file mode 100644 index 00000000000..78b57d7efc3 --- /dev/null +++ b/changelogs/unreleased/backup_aws_sse-c.yml @@ -0,0 +1,5 @@ +title: Add support for customer provided encryption keys for Amazon S3 remote backups +merge_request: 23797 +author: Pepijn Van Eeckhoudt +type: added + diff --git a/changelogs/unreleased/backup_restore_fix_issue_46891.yml b/changelogs/unreleased/backup_restore_fix_issue_46891.yml new file mode 100644 index 00000000000..b8fe3b1b861 --- /dev/null +++ b/changelogs/unreleased/backup_restore_fix_issue_46891.yml @@ -0,0 +1,5 @@ +--- +title: Modify file restore to rectify tar issue +merge_request: 24000 +author: +type: fixed diff --git a/changelogs/unreleased/bump-ingress-chart-112.yml b/changelogs/unreleased/bump-ingress-chart-112.yml new file mode 100644 index 00000000000..8a46fedb4b0 --- /dev/null +++ b/changelogs/unreleased/bump-ingress-chart-112.yml @@ -0,0 +1,5 @@ +--- +title: Bump nginx-ingress chart to 1.1.2 +merge_request: 24203 +author: +type: other diff --git a/changelogs/unreleased/cleanup-leagcy-artifact-migration.yml b/changelogs/unreleased/cleanup-leagcy-artifact-migration.yml new file mode 100644 index 00000000000..6e8dac97249 --- /dev/null +++ b/changelogs/unreleased/cleanup-leagcy-artifact-migration.yml @@ -0,0 +1,5 @@ +--- +title: Cleanup legacy artifact background migration +merge_request: 24144 +author: +type: other diff --git a/changelogs/unreleased/deprecated-force-reload.yml b/changelogs/unreleased/deprecated-force-reload.yml new file mode 100644 index 00000000000..2a0e97089e0 --- /dev/null +++ b/changelogs/unreleased/deprecated-force-reload.yml @@ -0,0 +1,6 @@ +--- +title: 'Fix deprecation: Passing an argument to force an association to reload is + now deprecated' +merge_request: 24136 +author: Jasper Maes +type: other diff --git a/changelogs/unreleased/error_tracking_feature_flag_fe.yml b/changelogs/unreleased/error_tracking_feature_flag_fe.yml new file mode 100644 index 00000000000..607929eb6b8 --- /dev/null +++ b/changelogs/unreleased/error_tracking_feature_flag_fe.yml @@ -0,0 +1,5 @@ +--- +title: Display a list of Sentry Issues in GitLab +merge_request: 23770 +author: +type: added diff --git a/changelogs/unreleased/features-document-graphicsmagick-source-installation.yml b/changelogs/unreleased/features-document-graphicsmagick-source-installation.yml new file mode 100644 index 00000000000..b224cace4bf --- /dev/null +++ b/changelogs/unreleased/features-document-graphicsmagick-source-installation.yml @@ -0,0 +1,5 @@ +--- +title: Document graphicsmagick installation for source installation +merge_request: 24404 +author: Alexis Reigel +type: added diff --git a/changelogs/unreleased/fix-403-page-is-rendered-but-404-is-the-response.yml b/changelogs/unreleased/fix-403-page-is-rendered-but-404-is-the-response.yml new file mode 100644 index 00000000000..eda69b32094 --- /dev/null +++ b/changelogs/unreleased/fix-403-page-is-rendered-but-404-is-the-response.yml @@ -0,0 +1,5 @@ +--- +title: Show the correct error page when access is denied +merge_request: 23932 +author: +type: fixed diff --git a/changelogs/unreleased/fix-auto-devops-domain-title-on-admin-settings.yml b/changelogs/unreleased/fix-auto-devops-domain-title-on-admin-settings.yml new file mode 100644 index 00000000000..bb0b193a846 --- /dev/null +++ b/changelogs/unreleased/fix-auto-devops-domain-title-on-admin-settings.yml @@ -0,0 +1,5 @@ +--- +title: Fixes Auto DevOps title on CI/CD admin settings +merge_request: 24249 +author: +type: other diff --git a/changelogs/unreleased/fix-udpate-head-pipeline-method.yml b/changelogs/unreleased/fix-udpate-head-pipeline-method.yml new file mode 100644 index 00000000000..8dbb9f8e42b --- /dev/null +++ b/changelogs/unreleased/fix-udpate-head-pipeline-method.yml @@ -0,0 +1,5 @@ +--- +title: Fix unexpected exception by failure of finding an actual head pipeline +merge_request: 24257 +author: +type: fixed diff --git a/changelogs/unreleased/fj-55882-fix-files-api-content-disposition.yml b/changelogs/unreleased/fj-55882-fix-files-api-content-disposition.yml new file mode 100644 index 00000000000..f64b29644b0 --- /dev/null +++ b/changelogs/unreleased/fj-55882-fix-files-api-content-disposition.yml @@ -0,0 +1,5 @@ +--- +title: Fix files/blob api endpoints content disposition +merge_request: 24267 +author: +type: fixed diff --git a/changelogs/unreleased/gitaly-update-1-13-0.yml b/changelogs/unreleased/gitaly-update-1-13-0.yml new file mode 100644 index 00000000000..73de25a532d --- /dev/null +++ b/changelogs/unreleased/gitaly-update-1-13-0.yml @@ -0,0 +1,5 @@ +--- +title: Upgrade Gitaly to 1.13.0 +merge_request: 24429 +author: +type: other diff --git a/changelogs/unreleased/gt-externalize-app-views-projects-project_members.yml b/changelogs/unreleased/gt-externalize-app-views-projects-project_members.yml new file mode 100644 index 00000000000..1acea10fcaa --- /dev/null +++ b/changelogs/unreleased/gt-externalize-app-views-projects-project_members.yml @@ -0,0 +1,5 @@ +--- +title: Externalize strings from `/app/views/projects/project_members` +merge_request: 23227 +author: Tao Wang +type: other diff --git a/changelogs/unreleased/gt-remove-unused-button-class.yml b/changelogs/unreleased/gt-remove-unused-button-class.yml new file mode 100644 index 00000000000..f7889e1d6f6 --- /dev/null +++ b/changelogs/unreleased/gt-remove-unused-button-class.yml @@ -0,0 +1,5 @@ +--- +title: Remove unused button classes `btn-create` and `comment-btn` +merge_request: 23232 +author: George Tsiolis +type: performance diff --git a/changelogs/unreleased/gt-rename-gray-theme-color-variables.yml b/changelogs/unreleased/gt-rename-gray-theme-color-variables.yml new file mode 100644 index 00000000000..b612bb3ee39 --- /dev/null +++ b/changelogs/unreleased/gt-rename-gray-theme-color-variables.yml @@ -0,0 +1,5 @@ +--- +title: Remove all `$theme-gray-{weight}` variables in favor of `$gray-{weight}` +merge_request: 24333 +author: George Tsiolis +type: other diff --git a/changelogs/unreleased/gt-update-string-struture-for-group-runners.yml b/changelogs/unreleased/gt-update-string-struture-for-group-runners.yml new file mode 100644 index 00000000000..fa06a78adae --- /dev/null +++ b/changelogs/unreleased/gt-update-string-struture-for-group-runners.yml @@ -0,0 +1,5 @@ +--- +title: Update string structure for available group runners +merge_request: 24187 +author: George Tsiolis +type: changed diff --git a/changelogs/unreleased/homepage-proj-descr-cutoff.yml b/changelogs/unreleased/homepage-proj-descr-cutoff.yml new file mode 100644 index 00000000000..837c01f6722 --- /dev/null +++ b/changelogs/unreleased/homepage-proj-descr-cutoff.yml @@ -0,0 +1,5 @@ +--- +title: Increase line height of project summaries +merge_request: +author: gfyoung +type: fixed diff --git a/changelogs/unreleased/iss-32584-preserve-line-number-fragment-after-redirect.yml b/changelogs/unreleased/iss-32584-preserve-line-number-fragment-after-redirect.yml new file mode 100644 index 00000000000..8025cd472bd --- /dev/null +++ b/changelogs/unreleased/iss-32584-preserve-line-number-fragment-after-redirect.yml @@ -0,0 +1,6 @@ +--- +title: Fix lost line number when navigating to a specific line in a protected file + before authenticating. +merge_request: 19165 +author: Scott Escue +type: fixed diff --git a/changelogs/unreleased/jivl-update-placeholder-sentry-config.yml b/changelogs/unreleased/jivl-update-placeholder-sentry-config.yml new file mode 100644 index 00000000000..eb860fd3905 --- /dev/null +++ b/changelogs/unreleased/jivl-update-placeholder-sentry-config.yml @@ -0,0 +1,5 @@ +--- +title: Update url placeholder for the sentry configuration page +merge_request: 24338 +author: +type: other diff --git a/changelogs/unreleased/knative-show-page.yml b/changelogs/unreleased/knative-show-page.yml new file mode 100644 index 00000000000..a48b754940f --- /dev/null +++ b/changelogs/unreleased/knative-show-page.yml @@ -0,0 +1,5 @@ +--- +title: Add Knative detailed view +merge_request: 23863 +author: Chris Baumbauer +type: added diff --git a/changelogs/unreleased/monospace-registry-tags.yml b/changelogs/unreleased/monospace-registry-tags.yml new file mode 100644 index 00000000000..b5992707d8c --- /dev/null +++ b/changelogs/unreleased/monospace-registry-tags.yml @@ -0,0 +1,5 @@ +--- +title: Use monospace font for registry table tag id and tag name +merge_request: 24205 +author: +type: other diff --git a/changelogs/unreleased/move-job-cancel-btn.yml b/changelogs/unreleased/move-job-cancel-btn.yml new file mode 100644 index 00000000000..41f8e1be5f8 --- /dev/null +++ b/changelogs/unreleased/move-job-cancel-btn.yml @@ -0,0 +1,5 @@ +--- +title: Move cancel & new issue button on job page +merge_request: 24074 +author: +type: changed diff --git a/changelogs/unreleased/mr-rebase-failing-tests.yml b/changelogs/unreleased/mr-rebase-failing-tests.yml new file mode 100644 index 00000000000..07ae05766b1 --- /dev/null +++ b/changelogs/unreleased/mr-rebase-failing-tests.yml @@ -0,0 +1,5 @@ +--- +title: Fixed rebase button not showing in merge request widget +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/notebook-multiple-outputs.yml b/changelogs/unreleased/notebook-multiple-outputs.yml new file mode 100644 index 00000000000..38cc52c0634 --- /dev/null +++ b/changelogs/unreleased/notebook-multiple-outputs.yml @@ -0,0 +1,5 @@ +--- +title: Support multiple outputs in jupyter notebooks +merge_request: +author: +type: changed diff --git a/changelogs/unreleased/notes-awards-double-tooltip-fix.yml b/changelogs/unreleased/notes-awards-double-tooltip-fix.yml new file mode 100644 index 00000000000..23338a60c2a --- /dev/null +++ b/changelogs/unreleased/notes-awards-double-tooltip-fix.yml @@ -0,0 +1,5 @@ +--- +title: Fixed double tooltips on note awards buttons +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/osw-fix-quick-suggestion-application-being-reverted.yml b/changelogs/unreleased/osw-fix-quick-suggestion-application-being-reverted.yml new file mode 100644 index 00000000000..1f80d7535a5 --- /dev/null +++ b/changelogs/unreleased/osw-fix-quick-suggestion-application-being-reverted.yml @@ -0,0 +1,5 @@ +--- +title: Adjust applied suggestion reverting previous changes +merge_request: 24250 +author: +type: fixed diff --git a/changelogs/unreleased/pl-reactive-caching-primary_key.yml b/changelogs/unreleased/pl-reactive-caching-primary_key.yml new file mode 100644 index 00000000000..a72933c19b1 --- /dev/null +++ b/changelogs/unreleased/pl-reactive-caching-primary_key.yml @@ -0,0 +1,5 @@ +--- +title: Enable caching for records which primary key is not `id` +merge_request: 24245 +author: +type: fixed diff --git a/changelogs/unreleased/profile-project-empty-state.yml b/changelogs/unreleased/profile-project-empty-state.yml new file mode 100644 index 00000000000..484306d5b98 --- /dev/null +++ b/changelogs/unreleased/profile-project-empty-state.yml @@ -0,0 +1,5 @@ +--- +title: Added empty project illustration and updated text to user profile overview +merge_request: 23973 +author: Fernando Arias +type: changed diff --git a/changelogs/unreleased/remove-cancel-all-button-in-job-list-view.yml b/changelogs/unreleased/remove-cancel-all-button-in-job-list-view.yml new file mode 100644 index 00000000000..06546bc5a8e --- /dev/null +++ b/changelogs/unreleased/remove-cancel-all-button-in-job-list-view.yml @@ -0,0 +1,5 @@ +--- +title: Remove Cancel all jobs button in general jobs list view +merge_request: +author: Jordi Llull +type: removed diff --git a/changelogs/unreleased/remove-gap-between-mr-tabs-and-file-header.yml b/changelogs/unreleased/remove-gap-between-mr-tabs-and-file-header.yml new file mode 100644 index 00000000000..ce8e1829b48 --- /dev/null +++ b/changelogs/unreleased/remove-gap-between-mr-tabs-and-file-header.yml @@ -0,0 +1,5 @@ +--- +title: Remove extra space between MR tab bar and sticky file headers +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/security-2770-verify-bundle-import-files.yml b/changelogs/unreleased/security-2770-verify-bundle-import-files.yml new file mode 100644 index 00000000000..dea40dd1ef1 --- /dev/null +++ b/changelogs/unreleased/security-2770-verify-bundle-import-files.yml @@ -0,0 +1,5 @@ +--- +title: Validate bundle files before unpacking them +merge_request: +author: +type: security diff --git a/changelogs/unreleased/sh-fix-backfill-project-repo-migration.yml b/changelogs/unreleased/sh-fix-backfill-project-repo-migration.yml new file mode 100644 index 00000000000..d1d4412eb50 --- /dev/null +++ b/changelogs/unreleased/sh-fix-backfill-project-repo-migration.yml @@ -0,0 +1,5 @@ +--- +title: Fix duplicate project disk path in BackfillLegacyProjectRepositories +merge_request: 24213 +author: +type: changed diff --git a/changelogs/unreleased/sh-fix-bitbucket-server-error-handling.yml b/changelogs/unreleased/sh-fix-bitbucket-server-error-handling.yml new file mode 100644 index 00000000000..87405fa0a78 --- /dev/null +++ b/changelogs/unreleased/sh-fix-bitbucket-server-error-handling.yml @@ -0,0 +1,5 @@ +--- +title: Fix Bitbucket Server importer error handling +merge_request: 24343 +author: +type: fixed diff --git a/changelogs/unreleased/shared_with_group_path.yml b/changelogs/unreleased/shared_with_group_path.yml new file mode 100644 index 00000000000..73ba9a9f30a --- /dev/null +++ b/changelogs/unreleased/shared_with_group_path.yml @@ -0,0 +1,5 @@ +--- +title: Add group full path to project's shared_with_groups +merge_request: 24052 +author: Mathieu Parent +type: added diff --git a/changelogs/unreleased/suggestion-dashes.yml b/changelogs/unreleased/suggestion-dashes.yml deleted file mode 100644 index e99ab30b263..00000000000 --- a/changelogs/unreleased/suggestion-dashes.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fixed diff suggestions removing dashes -merge_request: -author: -type: fixed diff --git a/changelogs/unreleased/tc-remove-20181218192239-migration.yml b/changelogs/unreleased/tc-remove-20181218192239-migration.yml new file mode 100644 index 00000000000..81e06a99c1f --- /dev/null +++ b/changelogs/unreleased/tc-remove-20181218192239-migration.yml @@ -0,0 +1,5 @@ +--- +title: Remove migration to backfill project_repositories for legacy storage projects +merge_request: 24299 +author: +type: removed diff --git a/changelogs/unreleased/twang2218-gitlab-ce-i18n-extract-app-views-search.yml b/changelogs/unreleased/twang2218-gitlab-ce-i18n-extract-app-views-search.yml new file mode 100644 index 00000000000..1af1fe09f33 --- /dev/null +++ b/changelogs/unreleased/twang2218-gitlab-ce-i18n-extract-app-views-search.yml @@ -0,0 +1,5 @@ +--- +title: 'i18n: externalize strings from ''app/views/search''' +merge_request: 24297 +author: Tao Wang +type: other diff --git a/changelogs/unreleased/update-gitlab-styles.yml b/changelogs/unreleased/update-gitlab-styles.yml new file mode 100644 index 00000000000..379f0ad4486 --- /dev/null +++ b/changelogs/unreleased/update-gitlab-styles.yml @@ -0,0 +1,5 @@ +--- +title: Update gitlab-styles to 2.5.1 +merge_request: 24336 +author: Jasper Maes +type: other diff --git a/changelogs/unreleased/update-sidekiq-cron.yml b/changelogs/unreleased/update-sidekiq-cron.yml new file mode 100644 index 00000000000..edce32e3753 --- /dev/null +++ b/changelogs/unreleased/update-sidekiq-cron.yml @@ -0,0 +1,6 @@ +--- +title: Update sidekiq-cron to 1.0.4 and use fugit to replace rufus-scheduler to parse + cron syntax +merge_request: 24235 +author: +type: other diff --git a/changelogs/unreleased/winh-add-list-dropdown-height.yml b/changelogs/unreleased/winh-add-list-dropdown-height.yml new file mode 100644 index 00000000000..6bcedc15cc9 --- /dev/null +++ b/changelogs/unreleased/winh-add-list-dropdown-height.yml @@ -0,0 +1,5 @@ +--- +title: Adjust height of "Add list" dropdown in issue boards +merge_request: 24227 +author: +type: fixed diff --git a/changelogs/unreleased/zj-feature-gate-set-project-path.yml b/changelogs/unreleased/zj-feature-gate-set-project-path.yml new file mode 100644 index 00000000000..b426a2f3fe7 --- /dev/null +++ b/changelogs/unreleased/zj-feature-gate-set-project-path.yml @@ -0,0 +1,5 @@ +--- +title: Allow setting of feature gates per project +merge_request: 24184 +author: +type: added diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index 7fe85f0e0d7..6fc33e8971e 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -635,6 +635,10 @@ production: &base # multipart_chunk_size: 104857600 # # Turns on AWS Server-Side Encryption with Amazon S3-Managed Keys for backups, this is optional # # encryption: 'AES256' + # # Turns on AWS Server-Side Encryption with Amazon Customer-Provided Encryption Keys for backups, this is optional + # # This should be set to the 256-bit, base64-encoded encryption key for Amazon S3 to use to encrypt or decrypt your data. + # # 'encryption' must also be set in order for this to have any effect. + # # encryption_key: '<base64 key>' # # Specifies Amazon S3 storage class to use for backups, this is optional # # storage_class: 'STANDARD' diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index db35fa96ea2..1aed41e02ab 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -392,6 +392,7 @@ Settings.backup['archive_permissions'] ||= 0600 Settings.backup['upload'] ||= Settingslogic.new({ 'remote_directory' => nil, 'connection' => nil }) Settings.backup['upload']['multipart_chunk_size'] ||= 104857600 Settings.backup['upload']['encryption'] ||= nil +Settings.backup['upload']['encryption_key'] ||= ENV['GITLAB_BACKUP_ENCRYPTION_KEY'] Settings.backup['upload']['storage_class'] ||= nil # diff --git a/config/initializers/sentry.rb b/config/initializers/sentry.rb index 2a6c5148f71..abc91c3ae51 100644 --- a/config/initializers/sentry.rb +++ b/config/initializers/sentry.rb @@ -19,7 +19,7 @@ def configure_sentry config.sanitize_fields = Rails.application.config.filter_parameters.map(&:to_s) # Sanitize authentication headers config.sanitize_http_headers = %w[Authorization Private-Token] - config.tags = { program: Gitlab::Sentry.program_context } + config.tags = { program: Gitlab.process_name } end end end diff --git a/config/initializers/tracing.rb b/config/initializers/tracing.rb new file mode 100644 index 00000000000..be95f30d075 --- /dev/null +++ b/config/initializers/tracing.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +if Gitlab::Tracing.enabled? + require 'opentracing' + + # In multi-processed clustered architectures (puma, unicorn) don't + # start tracing until the worker processes are spawned. This works + # around issues when the opentracing implementation spawns threads + Gitlab::Cluster::LifecycleEvents.on_worker_start do + tracer = Gitlab::Tracing::Factory.create_tracer(Gitlab.process_name, Gitlab::Tracing.connection_string) + OpenTracing.global_tracer = tracer if tracer + end +end diff --git a/config/jsdocs.config.js b/config/jsdocs.config.js new file mode 100644 index 00000000000..52635b1ce13 --- /dev/null +++ b/config/jsdocs.config.js @@ -0,0 +1,14 @@ +module.exports = { + source: { + include: ['app/assets/javascripts/'], + }, + opts: { + template: 'node_modules/docdash', + destination: 'jsdoc/', + recurse: true, + }, + docdash: { + search: true, + static: true, + }, +}; diff --git a/config/routes/project.rb b/config/routes/project.rb index cf5a57300cf..d9afb4e7bc8 100644 --- a/config/routes/project.rb +++ b/config/routes/project.rb @@ -247,6 +247,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do end namespace :serverless do + get '/functions/:environment_id/:id', to: 'functions#show' resources :functions, only: [:index] end @@ -255,8 +256,6 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do resources :jobs, only: [:index, :show], constraints: { id: /\d+/ } do collection do - post :cancel_all - resources :artifacts, only: [] do collection do get :latest_succeeded, @@ -442,6 +441,8 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do end end + resources :error_tracking, only: [:index], controller: :error_tracking + # Since both wiki and repository routing contains wildcard characters # its preferable to keep it below all other project routes draw :wiki diff --git a/danger/commit_messages/Dangerfile b/danger/commit_messages/Dangerfile index 6a5a75b6eba..c20c8b77e6a 100644 --- a/danger/commit_messages/Dangerfile +++ b/danger/commit_messages/Dangerfile @@ -2,6 +2,9 @@ require 'json' +URL_LIMIT_SUBJECT = "https://chris.beams.io/posts/git-commit/#limit-50" +URL_GIT_COMMIT = "https://chris.beams.io/posts/git-commit/" + # rubocop: disable Style/SignalException # rubocop: disable Metrics/CyclomaticComplexity # rubocop: disable Metrics/PerceivedComplexity @@ -101,10 +104,7 @@ def lint_commits(commits) elsif subject.length > 50 warn_commit( commit, - "This commit's subject line could be improved. " \ - 'Commit subjects are ideally no longer than roughly 50 characters, ' \ - 'though we allow up to 72 characters in the subject. ' \ - 'If possible, try to reduce the length of the subject to roughly 50 characters.' + "This commit's subject line is acceptable, but please try to [reduce it to 50 characters](#{URL_LIMIT_SUBJECT})." ) end @@ -196,7 +196,7 @@ def lint_commits(commits) One or more commit messages do not meet our Git commit message standards. For more information on how to write a good commit message, take a look at - [How to Write a Git Commit Message](https://chris.beams.io/posts/git-commit/). + [How to Write a Git Commit Message](#{URL_GIT_COMMIT}). Here is an example of a good commit message: diff --git a/db/fixtures/development/04_project.rb b/db/fixtures/development/04_project.rb index aa8686ac7d8..9a5f7cf8175 100644 --- a/db/fixtures/development/04_project.rb +++ b/db/fixtures/development/04_project.rb @@ -1,94 +1,136 @@ require './spec/support/sidekiq' +# rubocop:disable Rails/Output + Sidekiq::Testing.inline! do Gitlab::Seeder.quiet do - project_urls = [ - 'https://gitlab.com/gitlab-org/gitlab-test.git', - 'https://gitlab.com/gitlab-org/gitlab-shell.git', - 'https://gitlab.com/gnuwget/wget2.git', - 'https://gitlab.com/Commit451/LabCoat.git', - 'https://github.com/jashkenas/underscore.git', - 'https://github.com/flightjs/flight.git', - 'https://github.com/twitter/typeahead.js.git', - 'https://github.com/h5bp/html5-boilerplate.git', - 'https://github.com/google/material-design-lite.git', - 'https://github.com/jlevy/the-art-of-command-line.git', - 'https://github.com/FreeCodeCamp/freecodecamp.git', - 'https://github.com/google/deepdream.git', - 'https://github.com/jtleek/datasharing.git', - 'https://github.com/WebAssembly/design.git', - 'https://github.com/airbnb/javascript.git', - 'https://github.com/tessalt/echo-chamber-js.git', - 'https://github.com/atom/atom.git', - 'https://github.com/mattermost/mattermost-server.git', - 'https://github.com/purifycss/purifycss.git', - 'https://github.com/facebook/nuclide.git', - 'https://github.com/wbkd/awesome-d3.git', - 'https://github.com/kilimchoi/engineering-blogs.git', - 'https://github.com/gilbarbara/logos.git', - 'https://github.com/reduxjs/redux.git', - 'https://github.com/awslabs/s2n.git', - 'https://github.com/arkency/reactjs_koans.git', - 'https://github.com/twbs/bootstrap.git', - 'https://github.com/chjj/ttystudio.git', - 'https://github.com/MostlyAdequate/mostly-adequate-guide.git', - 'https://github.com/octocat/Spoon-Knife.git', - 'https://github.com/opencontainers/runc.git', - 'https://github.com/googlesamples/android-topeka.git' - ] - - # You can specify how many projects you need during seed execution - size = ENV['SIZE'].present? ? ENV['SIZE'].to_i : 8 - - project_urls.first(size).each_with_index do |url, i| - group_path, project_path = url.split('/')[-2..-1] - - group = Group.find_by(path: group_path) - - unless group - group = Group.new( - name: group_path.titleize, - path: group_path - ) - group.description = FFaker::Lorem.sentence - group.save - - group.add_owner(User.first) - end + Gitlab::Seeder.without_gitaly_timeout do + project_urls = %w[ + https://gitlab.com/gitlab-org/gitlab-test.git + https://gitlab.com/gitlab-org/gitlab-shell.git + https://gitlab.com/gnuwget/wget2.git + https://gitlab.com/Commit451/LabCoat.git + https://github.com/jashkenas/underscore.git + https://github.com/flightjs/flight.git + https://github.com/twitter/typeahead.js.git + https://github.com/h5bp/html5-boilerplate.git + https://github.com/google/material-design-lite.git + https://github.com/jlevy/the-art-of-command-line.git + https://github.com/FreeCodeCamp/freecodecamp.git + https://github.com/google/deepdream.git + https://github.com/jtleek/datasharing.git + https://github.com/WebAssembly/design.git + https://github.com/airbnb/javascript.git + https://github.com/tessalt/echo-chamber-js.git + https://github.com/atom/atom.git + https://github.com/mattermost/mattermost-server.git + https://github.com/purifycss/purifycss.git + https://github.com/facebook/nuclide.git + https://github.com/wbkd/awesome-d3.git + https://github.com/kilimchoi/engineering-blogs.git + https://github.com/gilbarbara/logos.git + https://github.com/reduxjs/redux.git + https://github.com/awslabs/s2n.git + https://github.com/arkency/reactjs_koans.git + https://github.com/twbs/bootstrap.git + https://github.com/chjj/ttystudio.git + https://github.com/MostlyAdequate/mostly-adequate-guide.git + https://github.com/octocat/Spoon-Knife.git + https://github.com/opencontainers/runc.git + https://github.com/googlesamples/android-topeka.git + ] - project_path.gsub!(".git", "") + large_project_urls = %w[ + https://github.com/torvalds/linux.git + https://gitlab.gnome.org/GNOME/gimp.git + https://gitlab.gnome.org/GNOME/gnome-mud.git + https://gitlab.com/fdroid/fdroidclient.git + https://gitlab.com/inkscape/inkscape.git + https://github.com/gnachman/iTerm2.git + ] - params = { - import_url: url, - namespace_id: group.id, - name: project_path.titleize, - description: FFaker::Lorem.sentence, - visibility_level: Gitlab::VisibilityLevel.values.sample, - skip_disk_validation: true - } + def create_project(url, force_latest_storage: false) + group_path, project_path = url.split('/')[-2..-1] - if i % 2 == 0 - params[:storage_version] = Project::LATEST_STORAGE_VERSION - end + group = Group.find_by(path: group_path) + + unless group + group = Group.new( + name: group_path.titleize, + path: group_path + ) + group.description = FFaker::Lorem.sentence + group.save + + group.add_owner(User.first) + end + + project_path.gsub!(".git", "") + + params = { + import_url: url, + namespace_id: group.id, + name: project_path.titleize, + description: FFaker::Lorem.sentence, + visibility_level: Gitlab::VisibilityLevel.values.sample, + skip_disk_validation: true + } + + if force_latest_storage + params[:storage_version] = Project::LATEST_STORAGE_VERSION + end + + project = nil - project = nil + Sidekiq::Worker.skipping_transaction_check do + project = Projects::CreateService.new(User.first, params).execute - Sidekiq::Worker.skipping_transaction_check do - project = Projects::CreateService.new(User.first, params).execute + # Seed-Fu runs this entire fixture in a transaction, so the `after_commit` + # hook won't run until after the fixture is loaded. That is too late + # since the Sidekiq::Testing block has already exited. Force clearing + # the `after_commit` queue to ensure the job is run now. + project.send(:_run_after_commit_queue) + project.import_state.send(:_run_after_commit_queue) + end - # Seed-Fu runs this entire fixture in a transaction, so the `after_commit` - # hook won't run until after the fixture is loaded. That is too late - # since the Sidekiq::Testing block has already exited. Force clearing - # the `after_commit` queue to ensure the job is run now. - project.send(:_run_after_commit_queue) - project.import_state.send(:_run_after_commit_queue) + if project.valid? && project.valid_repo? + print '.' + else + puts project.errors.full_messages + print 'F' + end end - if project.valid? && project.valid_repo? - print '.' - else - puts project.errors.full_messages - print 'F' + # You can specify how many projects you need during seed execution + size = ENV['SIZE'].present? ? ENV['SIZE'].to_i : 8 + + project_urls.first(size).each_with_index do |url, i| + create_project(url, force_latest_storage: i.even?) + end + + if ENV['LARGE_PROJECTS'].present? + large_project_urls.each(&method(:create_project)) + + if ENV['FORK'].present? + puts "\nGenerating forks" + + project_name = ENV['FORK'] == 'true' ? 'torvalds/linux' : ENV['FORK'] + + project = Project.find_by_full_path(project_name) + + User.offset(1).first(5).each do |user| + new_project = Projects::ForkService.new(project, user).execute + + if new_project.valid? && (new_project.valid_repo? || new_project.import_state.scheduled?) + print '.' + else + new_project.errors.full_messages.each do |error| + puts "#{new_project.full_path}: #{error}" + end + print 'F' + end + end + end end end end diff --git a/db/fixtures/development/10_merge_requests.rb b/db/fixtures/development/10_merge_requests.rb index 8bdc7c6556c..2051bcff8f0 100644 --- a/db/fixtures/development/10_merge_requests.rb +++ b/db/fixtures/development/10_merge_requests.rb @@ -27,6 +27,9 @@ Gitlab::Seeder.quiet do Sidekiq::Worker.skipping_transaction_check do MergeRequests::CreateService.new(project, developer, params).execute + rescue Repository::AmbiguousRefError + # Ignore pipelines creation errors for now, we can doing that after + # https://gitlab.com/gitlab-org/gitlab-ce/issues/55966. will be resolved. end print '.' end diff --git a/db/migrate/20190104182041_cleanup_legacy_artifact_migration.rb b/db/migrate/20190104182041_cleanup_legacy_artifact_migration.rb new file mode 100644 index 00000000000..11659846a06 --- /dev/null +++ b/db/migrate/20190104182041_cleanup_legacy_artifact_migration.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +class CleanupLegacyArtifactMigration < ActiveRecord::Migration[5.0] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + class Build < ActiveRecord::Base + include EachBatch + + self.table_name = 'ci_builds' + self.inheritance_column = :_type_disabled + + scope :with_legacy_artifacts, -> { where("artifacts_file <> ''") } + end + + def up + Gitlab::BackgroundMigration.steal('MigrateLegacyArtifacts') + + CleanupLegacyArtifactMigration::Build + .with_legacy_artifacts + .each_batch(of: 100) do |batch| + range = batch.pluck('MIN(id)', 'MAX(id)').first + + Gitlab::BackgroundMigration::MigrateLegacyArtifacts.new.perform(*range) + end + end + + def down + # no-op + end +end diff --git a/db/migrate/20190108192941_remove_partial_index_from_ci_builds_artifacts_file.rb b/db/migrate/20190108192941_remove_partial_index_from_ci_builds_artifacts_file.rb new file mode 100644 index 00000000000..073faf721ae --- /dev/null +++ b/db/migrate/20190108192941_remove_partial_index_from_ci_builds_artifacts_file.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +class RemovePartialIndexFromCiBuildsArtifactsFile < ActiveRecord::Migration[5.0] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + INDEX_NAME = 'partial_index_ci_builds_on_id_with_legacy_artifacts'.freeze + + disable_ddl_transaction! + + def up + remove_concurrent_index_by_name(:ci_builds, INDEX_NAME) + end + + def down + add_concurrent_index(:ci_builds, :id, where: "artifacts_file <> ''", name: INDEX_NAME) + end +end diff --git a/db/migrate/20190115054216_add_error_notification_sent_to_remote_mirrors.rb b/db/migrate/20190115054216_add_error_notification_sent_to_remote_mirrors.rb new file mode 100644 index 00000000000..d8f979a1848 --- /dev/null +++ b/db/migrate/20190115054216_add_error_notification_sent_to_remote_mirrors.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class AddErrorNotificationSentToRemoteMirrors < ActiveRecord::Migration[5.0] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def change + add_column :remote_mirrors, :error_notification_sent, :boolean + end +end diff --git a/db/post_migrate/20181218192239_backfill_project_repositories_for_legacy_storage_projects.rb b/db/post_migrate/20181218192239_backfill_project_repositories_for_legacy_storage_projects.rb deleted file mode 100644 index 42f96750789..00000000000 --- a/db/post_migrate/20181218192239_backfill_project_repositories_for_legacy_storage_projects.rb +++ /dev/null @@ -1,26 +0,0 @@ -# frozen_string_literal: true - -class BackfillProjectRepositoriesForLegacyStorageProjects < ActiveRecord::Migration[5.0] - include Gitlab::Database::MigrationHelpers - - DOWNTIME = false - BATCH_SIZE = 1_000 - DELAY_INTERVAL = 5.minutes - MIGRATION = 'BackfillLegacyProjectRepositories' - - disable_ddl_transaction! - - class Project < ActiveRecord::Base - include EachBatch - - self.table_name = 'projects' - end - - def up - queue_background_migration_jobs_by_range_at_intervals(Project, MIGRATION, DELAY_INTERVAL) - end - - def down - # no-op: since there could have been existing rows before the migration do not remove anything - end -end diff --git a/db/schema.rb b/db/schema.rb index 87826881d58..c6fef9b5d11 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: 20190103140724) do +ActiveRecord::Schema.define(version: 20190115054216) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -354,7 +354,6 @@ ActiveRecord::Schema.define(version: 20190103140724) do t.index ["commit_id", "status", "type"], name: "index_ci_builds_on_commit_id_and_status_and_type", using: :btree t.index ["commit_id", "type", "name", "ref"], name: "index_ci_builds_on_commit_id_and_type_and_name_and_ref", using: :btree t.index ["commit_id", "type", "ref"], name: "index_ci_builds_on_commit_id_and_type_and_ref", using: :btree - t.index ["id"], name: "partial_index_ci_builds_on_id_with_legacy_artifacts", where: "(artifacts_file <> ''::text)", using: :btree 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 @@ -1849,6 +1848,7 @@ ActiveRecord::Schema.define(version: 20190103140724) do t.string "encrypted_credentials_salt" t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.boolean "error_notification_sent" t.index ["last_successful_update_at"], name: "index_remote_mirrors_on_last_successful_update_at", using: :btree t.index ["project_id"], name: "index_remote_mirrors_on_project_id", using: :btree end diff --git a/doc/administration/gitaly/index.md b/doc/administration/gitaly/index.md index cf37eaa0b61..05c1923f0cb 100644 --- a/doc/administration/gitaly/index.md +++ b/doc/administration/gitaly/index.md @@ -217,6 +217,8 @@ repository from your GitLab server over HTTP. ## TLS support +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/22602) in GitLab 11.7. + Gitaly supports TLS credentials for GRPC authentication. To be able to communicate with a gitaly instance that listens for secure connections you will need to use `tls://` url scheme in the `gitaly_address` of the corresponding storage entry in the gitlab configuration. diff --git a/doc/administration/operations/unicorn.md b/doc/administration/operations/unicorn.md index bad61151bda..0e2079cb093 100644 --- a/doc/administration/operations/unicorn.md +++ b/doc/administration/operations/unicorn.md @@ -60,7 +60,17 @@ Unicorn master then automatically replaces the worker process. This is a robust way to handle memory leaks: Unicorn is designed to handle workers that 'crash' so no user requests will be dropped. The unicorn-worker-killer gem is designed to only terminate a worker process _in -between requests_, so no user requests are affected. +between requests_, so no user requests are affected. You can set the minimum and +maximum memory threshold (in bytes) for the Unicorn worker killer by +setting the following values `/etc/gitlab/gitlab.rb`: + +```ruby +unicorn['worker_memory_limit_min'] = "400 * 1 << 20" +unicorn['worker_memory_limit_max'] = "650 * 1 << 20" +``` + +Otherwise, you can set the `GITLAB_UNICORN_MEMORY_MIN` and `GITLAB_UNICORN_MEMORY_MIN` +[environment variables](../environment_variables.md). This is what a Unicorn worker memory restart looks like in unicorn_stderr.log. You see that worker 4 (PID 125918) is inspecting itself and decides to exit. diff --git a/doc/api/README.md b/doc/api/README.md index d481d0699e7..6c5bb1c0940 100644 --- a/doc/api/README.md +++ b/doc/api/README.md @@ -49,6 +49,7 @@ The following API resources are available: - [Projects](projects.md) including setting Webhooks - [Project access requests](access_requests.md) - [Project badges](project_badges.md) + - [Project clusters](project_clusters.md) - [Project-level variables](project_level_variables.md) - [Project import/export](project_import_export.md) - [Project members](members.md) @@ -68,6 +69,9 @@ The following API resources are available: - [Sidekiq metrics](sidekiq_metrics.md) - [System hooks](system_hooks.md) - [Tags](tags.md) +- [Releases](releases/index.md) +- Release Assets + - [Links](releases/links.md) - [Todos](todos.md) - [Users](users.md) - [Validate CI configuration](lint.md) (linting) diff --git a/doc/api/applications.md b/doc/api/applications.md index 7f95c136168..82955f0c1db 100644 --- a/doc/api/applications.md +++ b/doc/api/applications.md @@ -1,28 +1,36 @@ # Applications API -> [Introduced][ce-8160] in GitLab 10.5 +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8160) in GitLab 10.5. -[ce-8160]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8160 +Applications API operates on OAuth applications for: -Only admin user can use the Applications API. +- [Using GitLab as an authentication provider](../integration/oauth_provider.md). +- [Allowing access to GitLab resources on a user's behalf](oauth2.md). -## Create a application +NOTE: **Note:** +Only admin users can use the Applications API. -Create a application by posting a JSON payload. +## Create an application + +Create an application by posting a JSON payload. Returns `200` if the request succeeds. -``` +```text POST /applications ``` -| Attribute | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `name` | string | yes | The name of the application | -| `redirect_uri` | string | yes | The redirect URI of the application | -| `scopes` | string | yes | The scopes of the application | +Parameters: + +| Attribute | Type | Required | Description | +|:---------------|:-------|:---------|:---------------------------------| +| `name` | string | yes | Name of the application. | +| `redirect_uri` | string | yes | Redirect URI of the application. | +| `scopes` | string | yes | Scopes of the application. | + +Example request: -```bash +```sh curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" --data "name=MyApplication&redirect_uri=http://redirect.uri&scopes=" https://gitlab.example.com/api/v4/applications ``` @@ -42,11 +50,13 @@ Example response: List all registered applications. -``` +```text GET /applications ``` -```bash +Example request: + +```sh curl --request GET --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/applications ``` @@ -63,7 +73,8 @@ Example response: ] ``` -> Note: the `secret` value will not be exposed by this API. +NOTE: **Note:** +The `secret` value will not be exposed by this API. ## Delete an application @@ -71,7 +82,7 @@ Delete a specific application. Returns `204` if the request succeeds. -``` +```text DELETE /applications/:id ``` @@ -79,6 +90,8 @@ Parameters: - `id` (required) - The id of the application (not the application_id) -```bash +Example request: + +```sh curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/applications/:id ``` diff --git a/doc/api/avatar.md b/doc/api/avatar.md index aa6f7c185ae..e55fffba4b2 100644 --- a/doc/api/avatar.md +++ b/doc/api/avatar.md @@ -1,33 +1,41 @@ # Avatar API -> [Introduced][ce-19121] in GitLab 11.0 +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/19121) in GitLab 11.0. ## Get a single avatar URL -Get a single avatar URL for a given email address. If user with matching public -email address is not found, results from external avatar services are returned. -This endpoint can be accessed without authentication. In case public visibility -is restricted, response will be `403 Forbidden` when unauthenticated. +Get a single [avatar](../user/profile/index.md#profile-settings) URL for a user with the given email address. -``` +If: + +- No user with the given public email address is found, results from external avatar services are + returned. +- Public visibility is restricted, response will be `403 Forbidden` when unauthenticated. + +NOTE: **Note:** +This endpoint can be accessed without authentication. + +```text GET /avatar?email=admin@example.com ``` -| Attribute | Type | Required | Description | -| --------- | ------- | -------- | --------------------- | -| `email` | string | yes | Public email address of the user | -| `size` | integer | no | Single pixel dimension (since images are squares). Only used for avatar lookups at `Gravatar` or at the configured `Libravatar` server | +Parameters: -```bash -curl https://gitlab.example.com/api/v4/avatar?email=admin@example.com +| Attribute | Type | Required | Description | +|:----------|:--------|:---------|:----------------------------------------------------------------------------------------------------------------------------------------| +| `email` | string | yes | Public email address of the user. | +| `size` | integer | no | Single pixel dimension (since images are squares). Only used for avatar lookups at `Gravatar` or at the configured `Libravatar` server. | + +Example request: + +```sh +curl https://gitlab.example.com/api/v4/avatar?email=admin@example.com&size=32 ``` Example response: ```json { - "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon" + "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=64&d=identicon" } ``` - -[ce-19121]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/19121 diff --git a/doc/api/award_emoji.md b/doc/api/award_emoji.md index 41e39c31069..92936a277ac 100644 --- a/doc/api/award_emoji.md +++ b/doc/api/award_emoji.md @@ -1,19 +1,26 @@ # Award Emoji API -> [Introduced][ce-4575] in GitLab 8.9, Snippet support in 8.12 +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/4575) in GitLab 8.9. Snippet support added in 8.12. +An [awarded emoji](../user/award_emojis.md) tells a thousand words. -An awarded emoji tells a thousand words, and can be awarded on issues, merge -requests, snippets, and notes/comments. Issues, merge requests, snippets, and notes are further called -`awardables`. +Emoji can be awarded on the following (known as "awardables"): + +- [Issues](../user/project/issues/index.md) ([API](issues.md)). +- [Merge requests](../user/project/merge_requests/index.md) ([API](merge_requests.md)). +- [Snippets](../user/snippets.md) ([API](snippets.md)). + +Emoji can also [be awarded](../user/award_emojis.html#award-emoji-for-comments) on comments (also known as notes). See also [Notes API](notes.md). ## Issues, merge requests, and snippets +See [Award Emoji on Comments](#award-emoji-on-comments) for information on using these endpoints with comments. + ### List an awardable's award emoji -Gets a list of all award emoji +Get a list of all award emoji for a specified awardable. -``` +```text GET /projects/:id/issues/:issue_iid/award_emoji GET /projects/:id/merge_requests/:merge_request_iid/award_emoji GET /projects/:id/snippets/:snippet_id/award_emoji @@ -21,16 +28,18 @@ GET /projects/:id/snippets/:snippet_id/award_emoji 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 | -| `awardable_id` | integer | yes | The ID (`iid` for merge requests/issues, `id` for snippets) of an awardable | +| Attribute | Type | Required | Description | +|:---------------|:---------------|:---------|:-----------------------------------------------------------------------------| +| `id` | integer/string | yes | ID or [URL-encoded path of the project](README.md#namespaced-path-encoding). | +| `awardable_id` | integer | yes | ID (`iid` for merge requests/issues, `id` for snippets) of an awardable. | -```bash +Example request: + +```sh curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/1/issues/80/award_emoji ``` -Example Response: +Example response: ```json [ @@ -71,9 +80,9 @@ Example Response: ### Get single award emoji -Gets a single award emoji from an issue, snippet, or merge request. +Get a single award emoji from an issue, snippet, or merge request. -``` +```text GET /projects/:id/issues/:issue_iid/award_emoji/:award_id GET /projects/:id/merge_requests/:merge_request_iid/award_emoji/:award_id GET /projects/:id/snippets/:snippet_id/award_emoji/:award_id @@ -81,17 +90,19 @@ GET /projects/:id/snippets/:snippet_id/award_emoji/:award_id 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 | -| `awardable_id` | integer | yes | The ID (`iid` for merge requests/issues, `id` for snippets) of an awardable | -| `award_id` | integer | yes | The ID of the award emoji | +| Attribute | Type | Required | Description | +|:---------------|:---------------|:---------|:-----------------------------------------------------------------------------| +| `id` | integer/string | yes | ID or [URL-encoded path of the project](README.md#namespaced-path-encoding). | +| `awardable_id` | integer | yes | ID (`iid` for merge requests/issues, `id` for snippets) of an awardable. | +| `award_id` | integer | yes | ID of the award emoji. | + +Example request: -```bash +```sh curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/1/issues/80/award_emoji/1 ``` -Example Response: +Example response: ```json { @@ -114,9 +125,9 @@ Example Response: ### Award a new emoji -This end point creates an award emoji on the specified resource +Create an award emoji on the specified awardable. -``` +```text POST /projects/:id/issues/:issue_iid/award_emoji POST /projects/:id/merge_requests/:merge_request_iid/award_emoji POST /projects/:id/snippets/:snippet_id/award_emoji @@ -124,13 +135,13 @@ POST /projects/:id/snippets/:snippet_id/award_emoji 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 | -| `awardable_id` | integer | yes | The ID (`iid` for merge requests/issues, `id` for snippets) of an awardable | -| `name` | string | yes | The name of the emoji, without colons | +| Attribute | Type | Required | Description | +|:---------------|:---------------|:---------|:-----------------------------------------------------------------------------| +| `id` | integer/string | yes | ID or [URL-encoded path of the project](README.md#namespaced-path-encoding). | +| `awardable_id` | integer | yes | ID (`iid` for merge requests/issues, `id` for snippets) of an awardable. | +| `name` | string | yes | Name of the emoji without colons. | -```bash +```sh curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/1/issues/80/award_emoji?name=blowfish ``` @@ -157,10 +168,12 @@ Example Response: ### Delete an award emoji -Sometimes its just not meant to be, and you'll have to remove your award. Only available to -admins or the author of the award. +Sometimes it's just not meant to be and you'll have to remove the award. -``` +NOTE: **Note:** +Only available to administrators or the author of the award. + +```text DELETE /projects/:id/issues/:issue_iid/award_emoji/:award_id DELETE /projects/:id/merge_requests/:merge_request_iid/award_emoji/:award_id DELETE /projects/:id/snippets/:snippet_id/award_emoji/:award_id @@ -168,43 +181,47 @@ DELETE /projects/:id/snippets/:snippet_id/award_emoji/:award_id 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 an issue | -| `award_id` | integer | yes | The ID of an award_emoji | +| Attribute | Type | Required | Description | +|:---------------|:---------------|:---------|:-----------------------------------------------------------------------------| +| `id` | integer/string | yes | ID or [URL-encoded path of the project](README.md#namespaced-path-encoding). | +| `awardable_id` | integer | yes | ID (`iid` for merge requests/issues, `id` for snippets) of an awardable. | +| `award_id` | integer | yes | ID of an award emoji. | -```bash +```sh curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/1/issues/80/award_emoji/344 ``` -## Award Emoji on Notes +## Award Emoji on Comments -The endpoints documented above are available for Notes as well. Notes -are a sub-resource of Issues, Merge Requests, or Snippets. The examples below -describe working with Award Emoji on notes for an Issue, but can be -easily adapted for notes on a Merge Request. +Comments (also known as notes) are a sub-resource of issues, merge requests, and snippets. -### List a note's award emoji +NOTE: **Note:** +The examples below describe working with award emoji on comments for an issue, but can be +easily adapted for comments on a merge request. -``` +### List a comment's award emoji + +Get all award emoji for a comment (note). + +```text GET /projects/:id/issues/:issue_iid/notes/:note_id/award_emoji ``` 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 an issue | -| `note_id` | integer | yes | The ID of a note | +| Attribute | Type | Required | Description | +|:------------|:---------------|:---------|:-----------------------------------------------------------------------------| +| `id` | integer/string | yes | ID or [URL-encoded path of the project](README.md#namespaced-path-encoding). | +| `issue_iid` | integer | yes | Internal ID of an issue. | +| `note_id` | integer | yes | ID of a comment (note). | +Example request: -```bash +```sh curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/1/issues/80/notes/1/award_emoji ``` -Example Response: +Example response: ```json [ @@ -227,26 +244,30 @@ Example Response: ] ``` -### Get single note's award emoji +### Get an award emoji for a comment -``` +Get a single award emoji for a comment (note). + +```text GET /projects/:id/issues/:issue_iid/notes/:note_id/award_emoji/:award_id ``` 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 an issue | -| `note_id` | integer | yes | The ID of a note | -| `award_id` | integer | yes | The ID of the award emoji | +| Attribute | Type | Required | Description | +|:------------|:---------------|:---------|:-----------------------------------------------------------------------------| +| `id` | integer/string | yes | ID or [URL-encoded path of the project](README.md#namespaced-path-encoding). | +| `issue_iid` | integer | yes | Internal ID of an issue. | +| `note_id` | integer | yes | ID of a comment (note). | +| `award_id` | integer | yes | ID of the award emoji. | + +Example request: -```bash +```sh curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/1/issues/80/notes/1/award_emoji/2 ``` -Example Response: +Example response: ```json { @@ -267,26 +288,30 @@ Example Response: } ``` -### Award a new emoji on a note +### Award a new emoji on a comment -``` +Create an award emoji on the specified comment (note). + +```text POST /projects/:id/issues/:issue_iid/notes/:note_id/award_emoji ``` 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 an issue | -| `note_id` | integer | yes | The ID of a note | -| `name` | string | yes | The name of the emoji, without colons | +| Attribute | Type | Required | Description | +|:------------|:---------------|:---------|:-----------------------------------------------------------------------------| +| `id` | integer/string | yes | ID or [URL-encoded path of the project](README.md#namespaced-path-encoding). | +| `issue_iid` | integer | yes | Internal ID of an issue. | +| `note_id` | integer | yes | ID of a comment (note). | +| `name` | string | yes | Name of the emoji without colons. | -```bash +Example request: + +```sh curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/1/issues/80/notes/1/award_emoji?name=rocket ``` -Example Response: +Example response: ```json { @@ -307,26 +332,28 @@ Example Response: } ``` -### Delete an award emoji +### Delete an award emoji from a comment -Sometimes its just not meant to be, and you'll have to remove your award. Only available to -admins or the author of the award. +Sometimes it's just not meant to be and you'll have to remove the award. -``` +NOTE: **Note:** +Only available to administrators or the author of the award. + +```text DELETE /projects/:id/issues/:issue_iid/notes/:note_id/award_emoji/:award_id ``` 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 an issue | -| `note_id` | integer | yes | The ID of a note | -| `award_id` | integer | yes | The ID of an award_emoji | +| Attribute | Type | Required | Description | +|:------------|:---------------|:---------|:-----------------------------------------------------------------------------| +| `id` | integer/string | yes | ID or [URL-encoded path of the project](README.md#namespaced-path-encoding). | +| `issue_iid` | integer | yes | Internal ID of an issue. | +| `note_id` | integer | yes | ID of a comment (note). | +| `award_id` | integer | yes | ID of an award_emoji. | + +Example request: -```bash +```sh curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/1/issues/80/award_emoji/345 ``` - -[ce-4575]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/4575 diff --git a/doc/api/branches.md b/doc/api/branches.md index 3b55154887d..8d5f333ba77 100644 --- a/doc/api/branches.md +++ b/doc/api/branches.md @@ -1,21 +1,31 @@ # Branches API +This API operates on [repository branches](../user/project/repository/branches/index.md). + +TIP: **Tip:** +See also [Protected branches API](protected_branches.md). + ## List repository branches Get a list of repository branches from a project, sorted by name alphabetically. -This endpoint can be accessed without authentication if the repository is -publicly accessible. -``` +NOTE: **Note:** +This endpoint can be accessed without authentication if the repository is publicly accessible. + +```text GET /projects/:id/repository/branches ``` -| 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 | -| `search` | string | no | Return list of branches matching the search criteria. | +Parameters: + +| Attribute | Type | Required | Description | +|:----------|:---------------|:---------|:-------------------------------------------------------------------------------------------------------------| +| `id` | integer/string | yes | ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user. | +| `search` | string | no | Return list of branches matching the search criteria. | + +Example request: -```bash +```sh curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/5/repository/branches ``` @@ -53,19 +63,25 @@ Example response: ## Get single repository branch -Get a single project repository branch. This endpoint can be accessed without -authentication if the repository is publicly accessible. +Get a single project repository branch. -``` +NOTE: **Note:** +This endpoint can be accessed without authentication if the repository is publicly accessible. + +```text GET /projects/:id/repository/branches/:branch ``` -| 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 | -| `branch` | string | yes | The name of the branch | +Parameters: + +| Attribute | Type | Required | Description | +|:----------|:---------------|:---------|:-------------------------------------------------------------------------------------------------------------| +| `id` | integer/string | yes | ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user. | +| `branch` | string | yes | Name of the branch. | -```bash +Example request: + +```sh curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/5/repository/branches/master ``` @@ -100,120 +116,34 @@ Example response: ## Protect repository branch ->**Note:** This API endpoint is deprecated in favor of `POST /projects/:id/protected_branches`. - -Protects a single project repository branch. This is an idempotent function, -protecting an already protected repository branch still returns a `200 OK` -status code. - -``` -PUT /projects/:id/repository/branches/:branch/protect -``` - -```bash -curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/5/repository/branches/master/protect?developers_can_push=true&developers_can_merge=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 | -| `branch` | string | yes | The name of the branch | -| `developers_can_push` | boolean | no | Flag if developers can push to the branch | -| `developers_can_merge` | boolean | no | Flag if developers can merge to the branch | - -Example response: - -```json -{ - "commit": { - "author_email": "john@example.com", - "author_name": "John Smith", - "authored_date": "2012-06-27T05:51:39-07:00", - "committed_date": "2012-06-28T03:44:20-07:00", - "committer_email": "john@example.com", - "committer_name": "John Smith", - "id": "7b5c3cc8be40ee161ae89a06bba6229da1032a0c", - "short_id": "7b5c3cc", - "title": "add projects API", - "message": "add projects API", - "parent_ids": [ - "4ad91d3c1144c406e50c7b33bae684bd6837faf8" - ] - }, - "name": "master", - "merged": false, - "protected": true, - "default": true, - "developers_can_push": true, - "developers_can_merge": true, - "can_push": true -} -``` +See [`POST /projects/:id/protected_branches`](protected_branches.md#protect-repository-branches) for +information on protecting repository branches. ## Unprotect repository branch ->**Note:** This API endpoint is deprecated in favor of `DELETE /projects/:id/protected_branches/:name` +See [`DELETE /projects/:id/protected_branches/:name`](protected_branches.md#unprotect-repository-branches) +for information on unprotecting repository branches. -Unprotects a single project repository branch. This is an idempotent function, -unprotecting an already unprotected repository branch still returns a `200 OK` -status code. - -``` -PUT /projects/:id/repository/branches/:branch/unprotect -``` - -```bash -curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/5/repository/branches/master/unprotect -``` - -| 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 | -| `branch` | string | yes | The name of the branch | +## Create repository branch -Example response: +Create a new branch in the repository. -```json -{ - "commit": { - "author_email": "john@example.com", - "author_name": "John Smith", - "authored_date": "2012-06-27T05:51:39-07:00", - "committed_date": "2012-06-28T03:44:20-07:00", - "committer_email": "john@example.com", - "committer_name": "John Smith", - "id": "7b5c3cc8be40ee161ae89a06bba6229da1032a0c", - "short_id": "7b5c3cc", - "title": "add projects API", - "message": "add projects API", - "parent_ids": [ - "4ad91d3c1144c406e50c7b33bae684bd6837faf8" - ] - }, - "name": "master", - "merged": false, - "protected": false, - "default": true, - "developers_can_push": false, - "developers_can_merge": false, - "can_push": true -} +```text +POST /projects/:id/repository/branches ``` -## Create repository branch +Parameters: -``` -POST /projects/:id/repository/branches -``` +| Attribute | Type | Required | Description | +|:----------|:--------|:---------|:-------------------------------------------------------------------------------------------------------------| +| `id` | integer | yes | ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user. | +| `branch` | string | yes | Name of the branch. | +| `ref` | string | yes | Branch name or commit SHA to create branch from. | -| Attribute | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `id` | integer | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | -| `branch` | string | yes | The name of the branch | -| `ref` | string | yes | The branch name or commit SHA to create branch from | +Example request: -```bash -curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/repository/branches?branch=newbranch&ref=master" +```sh +curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/5/repository/branches?branch=newbranch&ref=master ``` Example response: @@ -247,36 +177,47 @@ Example response: ## Delete repository branch -``` +Delete a branch from the repository. + +NOTE: **Note:** +In the case of an error, an explanation message is provided. + +```text DELETE /projects/:id/repository/branches/:branch ``` -| 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 | -| `branch` | string | yes | The name of the branch | +Parameters: + +| Attribute | Type | Required | Description | +|:----------|:---------------|:---------|:-------------------------------------------------------------------------------------------------------------| +| `id` | integer/string | yes | ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user. | +| `branch` | string | yes | Name of the branch. | -In case of an error, an explaining message is provided. +Example request: -```bash -curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/repository/branches/newbranch" +```sh +curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/5/repository/branches/newbranch ``` ## Delete merged branches Will delete all branches that are merged into the project's default branch. -Protected branches will not be deleted as part of this operation. +NOTE: **Note:** +[Protected branches](../user/project/protected_branches.md) will not be deleted as part of this operation. -``` +```text DELETE /projects/:id/repository/merged_branches ``` -| 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 | +Parameters: + +| Attribute | Type | Required | Description | +|:----------|:---------------|:---------|:-------------------------------------------------------------------------------------------------------------| +| `id` | integer/string | yes | ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user. | +Example request: -```bash -curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/repository/merged_branches" +```sh +curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/5/repository/merged_branches ``` diff --git a/doc/api/features.md b/doc/api/features.md index 59f1005ef72..47f104e1f20 100644 --- a/doc/api/features.md +++ b/doc/api/features.md @@ -60,9 +60,10 @@ POST /features/:name | `value` | integer/string | yes | `true` or `false` to enable/disable, or an integer for percentage of time | | `feature_group` | string | no | A Feature group name | | `user` | string | no | A GitLab username | +| `project` | string | no | A projects path, for example 'gitlab-org/gitlab-ce' | -Note that you can enable or disable a feature for both a `feature_group` and a -`user` with a single API call. +Note that you can enable or disable a feature for a `feature_group`, a `user`, +and a `project` in a single API call. ```bash curl --data "value=30" --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/features/new_library diff --git a/doc/api/groups.md b/doc/api/groups.md index 2d9114c40ea..907b443d355 100644 --- a/doc/api/groups.md +++ b/doc/api/groups.md @@ -357,12 +357,14 @@ Example response: { "group_id": 4, "group_name": "Twitter", + "group_full_path": "twitter", "group_access_level": 30, "expires_at": null }, { "group_id": 3, "group_name": "Gitlab Org", + "group_full_path": "gitlab-org", "group_access_level": 10, "expires_at": "2018-08-14" } diff --git a/doc/api/import.md b/doc/api/import.md new file mode 100644 index 00000000000..9f8e0d232c6 --- /dev/null +++ b/doc/api/import.md @@ -0,0 +1,33 @@ +# Import API + +## Import repository from GitHub + +Import your projects from GitHub to GitLab via the API. + +``` +POST /import/github +``` + +| Attribute | Type | Required | Description | +|------------|---------|----------|---------------------| +| `personal_access_token` | string | yes | GitHub personal access token | +| `repo_id` | integer | yes | GitHub repository ID | +| `new_name` | string | no | New repo name | +| `target_namespace` | string | yes | Namespace to import repo into | + + +```bash +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --data "personal_access_token=abc123&repo_id=12345&target_namespace=root" https://gitlab.example.com/api/v4/import/github +``` + +Example response: + +```json +{ + "id": 27, + "name": "my-repo", + "full_path": "/root/my-repo", + "full_name": "Administrator / my-repo" +} +``` + diff --git a/doc/api/oauth2.md b/doc/api/oauth2.md index 6786c0c5b5c..6e156a14b25 100644 --- a/doc/api/oauth2.md +++ b/doc/api/oauth2.md @@ -1,126 +1,165 @@ # GitLab as an OAuth2 provider -This document covers using the [OAuth2](https://oauth.net/2/) protocol to allow other services access GitLab resources on user's behalf. +This document covers using the [OAuth2](https://oauth.net/2/) protocol to allow +other services to access GitLab resources on user's behalf. -If you want GitLab to be an OAuth authentication service provider to sign into other services please see the [OAuth2 provider](../integration/oauth_provider.md) -documentation. +If you want GitLab to be an OAuth authentication service provider to sign into +other services, see the [OAuth2 provider](../integration/oauth_provider.md) +documentation. This functionality is based on the +[doorkeeper Ruby gem](https://github.com/doorkeeper-gem/doorkeeper). -This functionality is based on [doorkeeper gem](https://github.com/doorkeeper-gem/doorkeeper). +## Supported OAuth2 flows -## Supported OAuth2 Flows +GitLab currently supports the following authorization flows: -GitLab currently supports following authorization flows: +- **Web application flow:** Most secure and common type of flow, designed for + applications with secure server-side. +- **Implicit grant flow:** This flow is designed for user-agent only apps (e.g., single + page web application running on GitLab Pages). +- **Resource owner password credentials flow:** To be used **only** for securely + hosted, first-party services. -- *Web Application Flow* - Most secure and common type of flow, designed for the applications with secure server-side. -- *Implicit Flow* - This flow is designed for user-agent only apps (e.g. single page web application running on GitLab Pages). -- *Resource Owner Password Credentials Flow* - To be used **only** for securely hosted, first-party services. +Refer to the [OAuth RFC](https://tools.ietf.org/html/rfc6749) to find out +how all those flows work and pick the right one for your use case. -Please refer to [OAuth RFC](https://tools.ietf.org/html/rfc6749) to find out in details how all those flows work and pick the right one for your use case. +Both **web application** and **implicit grant** flows require `application` to be +registered first via the `/profile/applications` page in your user's account. +During registration, by enabling proper scopes, you can limit the range of +resources which the `application` can access. Upon creation, you'll obtain the +`application` credentials: _Application ID_ and _Client Secret_ - **keep them secure**. -Both *web application* and *implicit* flows require `application` to be registered first via `/profile/applications` page -in your user's account. During registration, by enabling proper scopes you can limit the range of resources which the `application` can access. Upon creation -you'll obtain `application` credentials: _Application ID_ and _Client Secret_ - **keep them secure**. +CAUTION: **Important:** +OAuth specification advises sending the `state` parameter with each request to +`/oauth/authorize`. We highly recommended sending a unique value with each request +and validate it against the one in the redirect request. This is important in +order to prevent [CSRF attacks](https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF)). +The `state` parameter really should have been a requirement in the standard! ->**Important:** OAuth specification advises sending `state` parameter with each request to `/oauth/authorize`. We highly recommended to send a unique -value with each request and validate it against the one in redirect request. This is important to prevent [CSRF attacks]. The `state` param really should -have been a requirement in the standard! +In the following sections you will find detailed instructions on how to obtain +authorization with each flow. -In the following sections you will find detailed instructions on how to obtain authorization with each flow. +### Web application flow -### Web Application Flow +NOTE: **Note:** +Check the [RFC spec](https://tools.ietf.org/html/rfc6749#section-4.1) for a +detailed flow description. -Check [RFC spec](http://tools.ietf.org/html/rfc6749#section-4.1) for a detailed flow description +The web application flow is: -#### 1. Requesting authorization code +1. Request authorization code. To do that, you should redirect the user to the + `/oauth/authorize` endpoint with the following GET parameters: -To request the authorization code, you should redirect the user to the `/oauth/authorize` endpoint with following GET parameters: + ``` + https://gitlab.example.com/oauth/authorize?client_id=APP_ID&redirect_uri=REDIRECT_URI&response_type=code&state=YOUR_UNIQUE_STATE_HASH + ``` -``` -https://gitlab.example.com/oauth/authorize?client_id=APP_ID&redirect_uri=REDIRECT_URI&response_type=code&state=YOUR_UNIQUE_STATE_HASH -``` + This will ask the user to approve the applications access to their account and + then redirect back to the `REDIRECT_URI` you provided. The redirect will + include the GET `code` parameter, for example: -This will ask the user to approve the applications access to their account and then redirect back to the `REDIRECT_URI` you provided. The redirect will -include the GET `code` parameter, for example: + ``` + http://myapp.com/oauth/redirect?code=1234567890&state=YOUR_UNIQUE_STATE_HASH + ``` -`http://myapp.com/oauth/redirect?code=1234567890&state=YOUR_UNIQUE_STATE_HASH` + You should then use `code` to request an access token. -You should then use the `code` to request an access token. +1. Once you have the authorization code you can request an `access_token` using the + code. You can do that by using any HTTP client. In the following example, + we are using Ruby's `rest-client`: -#### 2. Requesting access token + ```ruby + parameters = 'client_id=APP_ID&client_secret=APP_SECRET&code=RETURNED_CODE&grant_type=authorization_code&redirect_uri=REDIRECT_URI' + RestClient.post 'http://gitlab.example.com/oauth/token', parameters + ``` -Once you have the authorization code you can request an `access_token` using the code, to do that you can use any HTTP client. In the following example, -we are using Ruby's `rest-client`: + Example response: -``` -parameters = 'client_id=APP_ID&client_secret=APP_SECRET&code=RETURNED_CODE&grant_type=authorization_code&redirect_uri=REDIRECT_URI' -RestClient.post 'http://gitlab.example.com/oauth/token', parameters + ```json + { + "access_token": "de6780bc506a0446309bd9362820ba8aed28aa506c71eedbe1c5c4f9dd350e54", + "token_type": "bearer", + "expires_in": 7200, + "refresh_token": "8257e65c97202ed1726cf9571600918f3bffb2544b26e00a61df9897668c33a1" + } + ``` -# The response will be -{ - "access_token": "de6780bc506a0446309bd9362820ba8aed28aa506c71eedbe1c5c4f9dd350e54", - "token_type": "bearer", - "expires_in": 7200, - "refresh_token": "8257e65c97202ed1726cf9571600918f3bffb2544b26e00a61df9897668c33a1" -} -``` ->**Note:** -The `redirect_uri` must match the `redirect_uri` used in the original authorization request. +NOTE: **Note:** +The `redirect_uri` must match the `redirect_uri` used in the original +authorization request. You can now make requests to the API with the access token returned. +### Implicit grant flow -### Implicit Grant - -Check [RFC spec](http://tools.ietf.org/html/rfc6749#section-4.2) for a detailed flow description. - -Unlike the web flow, the client receives an `access token` immediately as a result of the authorization request. The flow does not use client secret -or authorization code because all of the application code and storage is easily accessible, therefore __secrets__ can leak easily. +NOTE: **Note:** +Check the [RFC spec](https://tools.ietf.org/html/rfc6749#section-4.2) for a +detailed flow description. ->**Important:** Avoid using this flow for applications that store data outside of the GitLab instance. If you do, make sure to verify `application id` -associated with access token before granting access to the data +CAUTION: **Important:** +Avoid using this flow for applications that store data outside of the GitLab +instance. If you do, make sure to verify `application id` associated with the +access token before granting access to the data (see [/oauth/token/info](https://github.com/doorkeeper-gem/doorkeeper/wiki/API-endpoint-descriptions-and-examples#get----oauthtokeninfo)). +Unlike the web flow, the client receives an `access token` immediately as a +result of the authorization request. The flow does not use the client secret +or the authorization code because all of the application code and storage is +easily accessible, therefore secrets can leak easily. -#### 1. Requesting access token - -To request the access token, you should redirect the user to the `/oauth/authorize` endpoint using `token` response type: +To request the access token, you should redirect the user to the +`/oauth/authorize` endpoint using `token` response type: ``` https://gitlab.example.com/oauth/authorize?client_id=APP_ID&redirect_uri=REDIRECT_URI&response_type=token&state=YOUR_UNIQUE_STATE_HASH ``` -This will ask the user to approve the application's access to their account and then redirect back to the `REDIRECT_URI` you provided. The redirect -will include a fragment with `access_token` as well as token details in GET parameters, for example: +This will ask the user to approve the application's access to their account and +then redirect them back to the `REDIRECT_URI` you provided. The redirect +will include a fragment with `access_token` as well as token details in GET +parameters, for example: ``` http://myapp.com/oauth/redirect#access_token=ABCDExyz123&state=YOUR_UNIQUE_STATE_HASH&token_type=bearer&expires_in=3600 ``` -### Resource Owner Password Credentials +### Resource owner password credentials flow -Check [RFC spec](http://tools.ietf.org/html/rfc6749#section-4.3) for a detailed flow description. +NOTE: **Note:** +Check the [RFC spec](https://tools.ietf.org/html/rfc6749#section-4.3) for a +detailed flow description. -> **Deprecation notice:** Starting in GitLab 8.11, the Resource Owner Password Credentials has been *disabled* for users with two-factor authentication -turned on. These users can access the API using [personal access tokens] instead. +NOTE: **Note:** +The Resource Owner Password Credentials is disabled for users with [two-factor +authentication](../user/profile/account/two_factor_authentication.md) turned on. +These users can access the API using [personal access tokens](../user/profile/personal_access_tokens.md) +instead. -In this flow, a token is requested in exchange for the resource owner credentials (username and password). -The credentials should only be used when there is a high degree of trust between the resource owner and the client (e.g. the -client is part of the device operating system or a highly privileged application), and when other authorization grant types are not -available (such as an authorization code). +In this flow, a token is requested in exchange for the resource owner credentials +(username and password). ->**Important:** -Never store the user's credentials and only use this grant type when your client is deployed to a trusted environment, in 99% of cases [personal access tokens] -are a better choice. +The credentials should only be used when: -Even though this grant type requires direct client access to the resource owner credentials, the resource owner credentials are used -for a single request and are exchanged for an access token. This grant type can eliminate the need for the client to store the -resource owner credentials for future use, by exchanging the credentials with a long-lived access token or refresh token. +- There is a high degree of trust between the resource owner and the client. For + example, the client is part of the device operating system or a highly + privileged application. +- Other authorization grant types are not available (such as an authorization code). -#### 1. Requesting access token +CAUTION: **Important:** +Never store the user's credentials and only use this grant type when your client +is deployed to a trusted environment, in 99% of cases +[personal access tokens](../user/profile/personal_access_tokens.md) are a better +choice. -POST request to `/oauth/token` with parameters: +Even though this grant type requires direct client access to the resource owner +credentials, the resource owner credentials are used for a single request and +are exchanged for an access token. This grant type can eliminate the need for +the client to store the resource owner credentials for future use, by exchanging +the credentials with a long-lived access token or refresh token. -``` +To request an access token, you must make a POST request to `/oauth/token` with +the following parameters: + +```json { "grant_type" : "password", "username" : "user@example.com", @@ -128,6 +167,13 @@ POST request to `/oauth/token` with parameters: } ``` +Example cURL request: + +```sh +echo 'grant_type=password&username=<your_username>&password=<your_password>' > auth.txt +curl --data "@auth.txt" --request POST https://gitlab.example.com/oauth/token +``` + Then, you'll receive the access token back in the response: ``` @@ -138,7 +184,7 @@ Then, you'll receive the access token back in the response: } ``` -For testing you can use the oauth2 ruby gem: +For testing, you can use the `oauth2` Ruby gem: ``` client = OAuth2::Client.new('the_client_id', 'the_client_secret', :site => "http://example.com") @@ -148,7 +194,9 @@ puts access_token.token ## Access GitLab API with `access token` -The `access token` allows you to make requests to the API on a behalf of a user. You can pass the token either as GET parameter +The `access token` allows you to make requests to the API on behalf of a user. +You can pass the token either as GET parameter: + ``` GET https://gitlab.example.com/api/v4/user?access_token=OAUTH-TOKEN ``` @@ -159,5 +207,3 @@ or you can put the token to the Authorization header: curl --header "Authorization: Bearer OAUTH-TOKEN" https://gitlab.example.com/api/v4/user ``` -[personal access tokens]: ../user/profile/personal_access_tokens.md -[CSRF attacks]: http://www.oauthsecurity.com/#user-content-authorization-code-flow diff --git a/doc/api/project_clusters.md b/doc/api/project_clusters.md new file mode 100644 index 00000000000..c51a3564211 --- /dev/null +++ b/doc/api/project_clusters.md @@ -0,0 +1,346 @@ +# Project clusters API + +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/23922) +in GitLab 11.7. + +NOTE: **Note:** +User will need at least maintainer access to use these endpoints. + +## List project clusters + +Returns a list of project clusters. + +``` +GET /projects/:id/clusters +``` + +Parameters: + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of the project owned by the authenticated user | + +Example request: + +```bash +curl --header 'Private-Token: <your_access_token>' https://gitlab.example.com/api/v4/projects/26/clusters +``` + +Example response: + +```json +[ + { + "id":18, + "name":"cluster-1", + "created_at":"2019-01-02T20:18:12.563Z", + "provider_type":"user", + "platform_type":"kubernetes", + "environment_scope":"*", + "cluster_type":"project_type", + "user": + { + "id":1, + "name":"Administrator", + "username":"root", + "state":"active", + "avatar_url":"https://www.gravatar.com/avatar/4249f4df72b..", + "web_url":"https://gitlab.example.com/root" + }, + "platform_kubernetes": + { + "api_url":"https://104.197.68.152", + "namespace":"cluster-1-namespace", + "authorization_type":"rbac", + "ca_cert":"-----BEGIN CERTIFICATE-----\r\nhFiK1L61owwDQYJKoZIhvcNAQELBQAw\r\nLzEtMCsGA1UEAxMkZDA1YzQ1YjctNzdiMS00NDY0LThjNmEtMTQ0ZDJkZjM4ZDBj\r\nMB4XDTE4MTIyNzIwMDM1MVoXDTIzMTIyNjIxMDM1MVowLzEtMCsGA1UEAxMkZDA1\r\nYzQ1YjctNzdiMS00NDY0LThjNmEtMTQ0ZDJkZjM.......-----END CERTIFICATE-----" + } + }, + { + "id":19, + "name":"cluster-2", + ... + } +] +``` + +## Get a single project cluster + +Gets a single project cluster. + +```bash +GET /projects/:id/clusters/:cluster_id +``` + +Parameters: + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of the project owned by the authenticated user | +| `cluster_id` | integer | yes | The ID of the cluster | + +Example request: + +```bash +curl --header 'Private-Token: <your_access_token>' https://gitlab.example.com/api/v4/projects/26/clusters/18 +``` + +Example response: + +```json +{ + "id":18, + "name":"cluster-1", + "created_at":"2019-01-02T20:18:12.563Z", + "provider_type":"user", + "platform_type":"kubernetes", + "environment_scope":"*", + "cluster_type":"project_type", + "user": + { + "id":1, + "name":"Administrator", + "username":"root", + "state":"active", + "avatar_url":"https://www.gravatar.com/avatar/4249f4df72b..", + "web_url":"https://gitlab.example.com/root" + }, + "platform_kubernetes": + { + "api_url":"https://104.197.68.152", + "namespace":"cluster-1-namespace", + "authorization_type":"rbac", + "ca_cert":"-----BEGIN CERTIFICATE-----\r\nhFiK1L61owwDQYJKoZIhvcNAQELBQAw\r\nLzEtMCsGA1UEAxMkZDA1YzQ1YjctNzdiMS00NDY0LThjNmEtMTQ0ZDJkZjM4ZDBj\r\nMB4XDTE4MTIyNzIwMDM1MVoXDTIzMTIyNjIxMDM1MVowLzEtMCsGA1UEAxMkZDA1\r\nYzQ1YjctNzdiMS00NDY0LThjNmEtMTQ0ZDJkZjM.......-----END CERTIFICATE-----" + }, + "project": + { + "id":26, + "description":"", + "name":"project-with-clusters-api", + "name_with_namespace":"Administrator / project-with-clusters-api", + "path":"project-with-clusters-api", + "path_with_namespace":"root/project-with-clusters-api", + "created_at":"2019-01-02T20:13:32.600Z", + "default_branch":null, + "tag_list":[], + "ssh_url_to_repo":"ssh://gitlab.example.com/root/project-with-clusters-api.git", + "http_url_to_repo":"https://gitlab.example.com/root/project-with-clusters-api.git", + "web_url":"https://gitlab.example.com/root/project-with-clusters-api", + "readme_url":null, + "avatar_url":null, + "star_count":0, + "forks_count":0, + "last_activity_at":"2019-01-02T20:13:32.600Z", + "namespace": + { + "id":1, + "name":"root", + "path":"root", + "kind":"user", + "full_path":"root", + "parent_id":null + } + } +} +``` + +## Add existing cluster to project + +Adds an existing Kubernetes cluster to the project. + +```bash +POST /projects/:id/clusters/user +``` + +Parameters: + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of the project owned by the authenticated user | +| `name` | String | yes | The name of the cluster | +| `enabled` | Boolean | no | Determines if cluster is active or not, defaults to true | +| `platform_kubernetes_attributes[api_url]` | String | yes | The URL to access the Kubernetes API | +| `platform_kubernetes_attributes[token]` | String | yes | The token to authenticate against Kubernetes | +| `platform_kubernetes_attributes[ca_cert]` | String | no | TLS certificate (needed if API is using a self-signed TLS certificate | +| `platform_kubernetes_attributes[namespace]` | String | no | The unique namespace related to the project | +| `platform_kubernetes_attributes[authorization_type]` | String | no | The cluster authorization type: `rbac`, `abac` or `unknown_authorization`. Defaults to `rbac`. | + +Example request: + +```bash +curl --header 'Private-Token: <your_access_token>' https://gitlab.example.com/api/v4/projects/26/clusters/user \ +-H "Accept: application/json" \ +-H "Content-Type:application/json" \ +-X POST --data '{"name":"cluster-5", "platform_kubernetes_attributes":{"api_url":"https://35.111.51.20","token":"12345","namespace":"cluster-5-namespace","ca_cert":"-----BEGIN CERTIFICATE-----\r\nhFiK1L61owwDQYJKoZIhvcNAQELBQAw\r\nLzEtMCsGA1UEAxMkZDA1YzQ1YjctNzdiMS00NDY0LThjNmEtMTQ0ZDJkZjM4ZDBj\r\nMB4XDTE4MTIyNzIwMDM1MVoXDTIzMTIyNjIxMDM1MVowLzEtMCsGA1UEAxMkZDA1\r\nYzQ1YjctNzdiMS00NDY0LThjNmEtMTQ0ZDJkZjM.......-----END CERTIFICATE-----"}}' +``` + +Example response: + +```json +{ + "id":24, + "name":"cluster-5", + "created_at":"2019-01-03T21:53:40.610Z", + "provider_type":"user", + "platform_type":"kubernetes", + "environment_scope":"*", + "cluster_type":"project_type", + "user": + { + "id":1, + "name":"Administrator", + "username":"root", + "state":"active", + "avatar_url":"https://www.gravatar.com/avatar/4249f4df72b..", + "web_url":"https://gitlab.example.com/root" + }, + "platform_kubernetes": + { + "api_url":"https://35.111.51.20", + "namespace":"cluster-5-namespace", + "authorization_type":"rbac", + "ca_cert":"-----BEGIN CERTIFICATE-----\r\nhFiK1L61owwDQYJKoZIhvcNAQELBQAw\r\nLzEtMCsGA1UEAxMkZDA1YzQ1YjctNzdiMS00NDY0LThjNmEtMTQ0ZDJkZjM4ZDBj\r\nMB4XDTE4MTIyNzIwMDM1MVoXDTIzMTIyNjIxMDM1MVowLzEtMCsGA1UEAxMkZDA1\r\nYzQ1YjctNzdiMS00NDY0LThjNmEtMTQ0ZDJkZjM.......-----END CERTIFICATE-----" + }, + "project": + { + "id":26, + "description":"", + "name":"project-with-clusters-api", + "name_with_namespace":"Administrator / project-with-clusters-api", + "path":"project-with-clusters-api", + "path_with_namespace":"root/project-with-clusters-api", + "created_at":"2019-01-02T20:13:32.600Z", + "default_branch":null, + "tag_list":[], + "ssh_url_to_repo":"ssh:://gitlab.example.com/root/project-with-clusters-api.git", + "http_url_to_repo":"https://gitlab.example.com/root/project-with-clusters-api.git", + "web_url":"https://gitlab.example.com/root/project-with-clusters-api", + "readme_url":null, + "avatar_url":null, + "star_count":0, + "forks_count":0, + "last_activity_at":"2019-01-02T20:13:32.600Z", + "namespace": + { + "id":1, + "name":"root", + "path":"root", + "kind":"user", + "full_path":"root", + "parent_id":null + } + } +} +``` + +## Edit project cluster + +Updates an existing project cluster. + +```bash +PUT /projects/:id/clusters/:cluster_id +``` + +Parameters: + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of the project owned by the authenticated user | +| `name` | String | no | The name of the cluster | +| `platform_kubernetes_attributes[api_url]` | String | no | The URL to access the Kubernetes API | +| `platform_kubernetes_attributes[token]` | String | no | The token to authenticate against Kubernetes | +| `platform_kubernetes_attributes[ca_cert]` | String | no | TLS certificate (needed if API is using a self-signed TLS certificate | +| `platform_kubernetes_attributes[namespace]` | String | no | The unique namespace related to the project | + +NOTE: **Note:** +`name`, `api_url`, `ca_cert` and `token` can only be updated if the cluster was added +through the ["Add an existing Kubernetes Cluster"](../user/project/clusters/index.md#adding-an-existing-kubernetes-cluster) option or +through the ["Add existing cluster to project"](#add-existing-cluster-to-project) endpoint. + +Example request: + +```bash +curl --header 'Private-Token: <your_access_token>' https://gitlab.example.com/api/v4/projects/26/clusters/24 \ +-H "Content-Type:application/json" \ +-X PUT --data '{"name":"new-cluster-name","api_url":"https://new-api-url.com"}' +``` + +Example response: + +```json +{ + "id":24, + "name":"new-cluster-name", + "created_at":"2019-01-03T21:53:40.610Z", + "provider_type":"user", + "platform_type":"kubernetes", + "environment_scope":"*", + "cluster_type":"project_type", + "user": + { + "id":1, + "name":"Administrator", + "username":"root", + "state":"active", + "avatar_url":"https://www.gravatar.com/avatar/4249f4df72b..", + "web_url":"https://gitlab.example.com/root" + }, + "platform_kubernetes": + { + "api_url":"https://new-api-url.com", + "namespace":"cluster-5-namespace", + "authorization_type":"rbac", + "ca_cert":null + }, + "project": + { + "id":26, + "description":"", + "name":"project-with-clusters-api", + "name_with_namespace":"Administrator / project-with-clusters-api", + "path":"project-with-clusters-api", + "path_with_namespace":"root/project-with-clusters-api", + "created_at":"2019-01-02T20:13:32.600Z", + "default_branch":null, + "tag_list":[], + "ssh_url_to_repo":"ssh:://gitlab.example.com/root/project-with-clusters-api.git", + "http_url_to_repo":"https://gitlab.example.com/root/project-with-clusters-api.git", + "web_url":"https://gitlab.example.com/root/project-with-clusters-api", + "readme_url":null, + "avatar_url":null, + "star_count":0, + "forks_count":0, + "last_activity_at":"2019-01-02T20:13:32.600Z", + "namespace": + { + "id":1, + "name":"root", + "path":"root", + "kind":"user", + "full_path":"root", + "parent_id":null + } + } +} + +``` + +## Delete project cluster + +Deletes an existing project cluster. + +``` +DELETE /projects/:id/clusters/:cluster_id +``` + +Parameters: + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of the project owned by the authenticated user | +| `cluster_id` | integer | yes | The ID of the cluster | + +Example request: + +```bash +curl --header 'Private-Token: <your_access_token>' https://gitlab.example.com/api/v4/projects/26/clusters/23' +``` diff --git a/doc/api/projects.md b/doc/api/projects.md index 538cd34de43..1296b435792 100644 --- a/doc/api/projects.md +++ b/doc/api/projects.md @@ -525,11 +525,13 @@ GET /projects/:id { "group_id": 4, "group_name": "Twitter", + "group_full_path": "twitter", "group_access_level": 30 }, { "group_id": 3, "group_name": "Gitlab Org", + "group_full_path": "gitlab-org", "group_access_level": 10 } ], diff --git a/doc/api/releases.md b/doc/api/releases/index.md index 4613fe3482a..943109a3ea9 100644 --- a/doc/api/releases.md +++ b/doc/api/releases/index.md @@ -1,7 +1,8 @@ # Releases API > - [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/41766) in GitLab 11.7. -> - Using this API you can manipulate GitLab's [Release](../user/project/releases/index.md) entries. +> - Using this API you can manipulate GitLab's [Release](../../user/project/releases/index.md) entries. +> - For manipulating links as a release asset, see [Release Links API](links.md) ## List Releases @@ -241,7 +242,7 @@ POST /projects/:id/releases | `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding). | | `name` | string | yes | The release name. | | `tag_name` | string | yes | The tag where the release will be created from. | -| `description` | string | no | The description of the release. You can use [markdown](../user/markdown.md). | +| `description` | string | yes | The description of the release. You can use [markdown](../user/markdown.md). | | `ref` | string | no | If `tag_name` doesn't exist, the release will be created from `ref`. It can be a commit SHA, another tag name, or a branch name. | | `assets:links`| array of hash | no | An array of assets links. | | `assets:links:name`| string | no (if `assets:links` specified, it's required) | The name of the link. | @@ -331,8 +332,8 @@ PUT /projects/:id/releases/:tag_name | Attribute | Type | Required | Description | | ------------- | -------------- | -------- | --------------------------------------- | | `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding). | +| `tag_name` | string | yes | The tag where the release will be created from. | | `name` | string | no | The release name. | -| `tag_name` | string | no | The tag where the release will be created from. | | `description` | string | no | The description of the release. You can use [markdown](../user/markdown.md). | Example request: diff --git a/doc/api/releases/links.md b/doc/api/releases/links.md new file mode 100644 index 00000000000..ae99f3bd8b6 --- /dev/null +++ b/doc/api/releases/links.md @@ -0,0 +1,177 @@ +# Release links API + +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/41766) in GitLab 11.7. + +Using this API you can manipulate GitLab's [Release](../../user/project/releases/index.md) links. For manipulating other Release assets, see [Release API](index.md). + +## Get links + +Get assets as links from a Release. + +``` +GET /projects/:id/releases/:tag_name/assets/links +``` + +| Attribute | Type | Required | Description | +| ------------- | -------------- | -------- | --------------------------------------- | +| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding). | +| `tag_name` | string | yes | The tag associated with the Release. | + +Example request: + +```sh +curl --header "PRIVATE-TOKEN: gDybLx3yrUK_HLp3qPjS" "http://localhost:3000/api/v4/projects/24/releases/v0.1/assets/links" +``` + +Example response: + +```json +[ + { + "id":2, + "name":"awesome-v0.2.msi", + "url":"http://192.168.10.15:3000/msi", + "external":true + }, + { + "id":1, + "name":"awesome-v0.2.dmg", + "url":"http://192.168.10.15:3000", + "external":true + } +] +``` + +## Get a link + +Get an asset as a link from a Release. + +``` +GET /projects/:id/releases/:tag_name/assets/links/:link_id +``` + +| Attribute | Type | Required | Description | +| ------------- | -------------- | -------- | --------------------------------------- | +| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding). | +| `tag_name` | string | yes | The tag associated with the Release. | +| `link_id` | integer | yes | The id of the link. | + +Example request: + +```sh +curl --header "PRIVATE-TOKEN: gDybLx3yrUK_HLp3qPjS" "http://localhost:3000/api/v4/projects/24/releases/v0.1/assets/links/1" +``` + +Example response: + +```json +{ + "id":1, + "name":"awesome-v0.2.dmg", + "url":"http://192.168.10.15:3000", + "external":true +} +``` + +## Create a link + +Create an asset as a link from a Release. + +``` +POST /projects/:id/releases/:tag_name/assets/links +``` + +| Attribute | Type | Required | Description | +| ------------- | -------------- | -------- | --------------------------------------- | +| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding). | +| `tag_name` | string | yes | The tag associated with the Release. | +| `name` | string | yes | The name of the link. | +| `url` | string | yes | The URL of the link. | + +Example request: + +```sh +curl --request POST \ + --header "PRIVATE-TOKEN: gDybLx3yrUK_HLp3qPjS" \ + --data name="awesome-v0.2.dmg" \ + --data url="http://192.168.10.15:3000" \ + "http://localhost:3000/api/v4/projects/24/releases/v0.1/assets/links" +``` + +Example response: + +```json +{ + "id":1, + "name":"awesome-v0.2.dmg", + "url":"http://192.168.10.15:3000", + "external":true +} +``` + +## Update a link + +Update an asset as a link from a Release. + +``` +PUT /projects/:id/releases/:tag_name/assets/links/:link_id +``` + +| Attribute | Type | Required | Description | +| ------------- | -------------- | -------- | --------------------------------------- | +| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding). | +| `tag_name` | string | yes | The tag associated with the Release. | +| `link_id` | integer | yes | The id of the link. | +| `name` | string | no | The name of the link. | +| `url` | string | no | The URL of the link. | + +NOTE: **NOTE** +You have to specify at least one of `name` or `url` + +Example request: + +```sh +curl --request PUT --data name="new name" --header "PRIVATE-TOKEN: gDybLx3yrUK_HLp3qPjS" "http://localhost:3000/api/v4/projects/24/releases/v0.1/assets/links/1" +``` + +Example response: + +```json +{ + "id":1, + "name":"new name", + "url":"http://192.168.10.15:3000", + "external":true +} +``` + +## Delete a link + +Delete an asset as a link from a Release. + +``` +DELETE /projects/:id/releases/:tag_name/assets/links/:link_id +``` + +| Attribute | Type | Required | Description | +| ------------- | -------------- | -------- | --------------------------------------- | +| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding). | +| `tag_name` | string | yes | The tag associated with the Release. | +| `link_id` | integer | yes | The id of the link. | + +Example request: + +```sh +curl --request DELETE --header "PRIVATE-TOKEN: gDybLx3yrUK_HLp3qPjS" "http://localhost:3000/api/v4/projects/24/releases/v0.1/assets/links/1" +``` + +Example response: + +```json +{ + "id":1, + "name":"new name", + "url":"http://192.168.10.15:3000", + "external":true +} +``` diff --git a/doc/ci/environments.md b/doc/ci/environments.md index 315d0c5e7ef..010c579b83e 100644 --- a/doc/ci/environments.md +++ b/doc/ci/environments.md @@ -419,7 +419,7 @@ and/or `production`) you can see this information in the merge request itself. > Introduced in GitLab 8.17. In GitLab 11.5 the file links are surfaced to the merge request widget. -You can specify a Route Map to get GitLab to show "View on <environment URL>" +You can specify a Route Map to get GitLab to show **View on ...** buttons to go directly from a file to that file's representation on the [deployed website via Review Apps](review_apps/index.md). diff --git a/doc/ci/runners/README.md b/doc/ci/runners/README.md index 9c9ea651678..c742dc61368 100644 --- a/doc/ci/runners/README.md +++ b/doc/ci/runners/README.md @@ -135,6 +135,14 @@ Consider that if you don't lock your specific Runner to a specific project, any user with Maintainer role in you project can assign your Runner to another arbitrary project without requiring your authorization, so use it with caution. +CAUTION: **Caution:** +Never add a private Runner that you're using in your private projects to a +project that you share with other people. + +CAUTION: **Caution:** +Never use a Runner from a project which has multiple maintainers in your +private project. + An admin can enable/disable a specific Runner for projects: 1. Navigate to **Admin > Runners** diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index efee2852eb8..d7ab6fc506d 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -57,6 +57,7 @@ A job is defined by a list of parameters that define the job behavior. |---------------|----------|-------------| | [script](#script) | yes | Defines a shell script which is executed by Runner | | [extends](#extends) | no | Defines a configuration entry that this job is going to inherit from | +| [include](#include) | no | Defines a configuration entry that allows this job to include external YAML files | | [image](#image-and-services) | no | Use docker image, covered in [Using Docker Images](../docker/using_docker_images.md#define-image-and-services-from-gitlab-ciyml) | | [services](#image-and-services) | no | Use docker services, covered in [Using Docker Images](../docker/using_docker_images.md#define-image-and-services-from-gitlab-ciyml) | | [stage](#stage) | no | Defines a job stage (default: `test`) | @@ -77,118 +78,6 @@ A job is defined by a list of parameters that define the job behavior. | [retry](#retry) | no | Define when and how many times a job can be auto-retried in case of a failure | | [parallel](#parallel) | no | Defines how many instances of a job should be run in parallel | -### `extends` - -> Introduced in GitLab 11.3. - -`extends` defines an entry name that a job that uses `extends` is going to -inherit from. - -It is an alternative to using [YAML anchors](#anchors) and is a little -more flexible and readable: - -```yaml -.tests: - script: rake test - stage: test - only: - refs: - - branches - -rspec: - extends: .tests - script: rake rspec - only: - variables: - - $RSPEC -``` - -In the example above, the `rspec` job inherits from the `.tests` template job. -GitLab will perform a reverse deep merge based on the keys. GitLab will: - -- Merge the `rspec` contents into `.tests` recursively. -- Not merge the values of the keys. - -This results in the following `rspec` job: - -```yaml -rspec: - script: rake rspec - stage: test - only: - refs: - - branches - variables: - - $RSPEC -``` - -NOTE: **Note:** -Note that `script: rake test` has been overwritten by `script: rake rspec`. - -If you do want to include the `rake test`, have a look at [before_script-and-after_script](#before_script-and-after_script). - -`.tests` in this example is a [hidden key](#hidden-keys-jobs), but it's -possible to inherit from regular jobs as well. - -`extends` supports multi-level inheritance, however it is not recommended to -use more than three levels. The maximum nesting level that is supported is 10. -The following example has two levels of inheritance: - -```yaml -.tests: - only: - - pushes - -.rspec: - extends: .tests - script: rake rspec - -rspec 1: - variables: - RSPEC_SUITE: '1' - extends: .rspec - -rspec 2: - variables: - RSPEC_SUITE: '2' - extends: .rspec - -spinach: - extends: .tests - script: rake spinach -``` - -`extends` works across configuration files combined with [`include`](#include). - -### `pages` - -`pages` is a special job that is used to upload static content to GitLab that -can be used to serve your website. It has a special syntax, so the two -requirements below must be met: - -1. Any static content must be placed under a `public/` directory -1. `artifacts` with a path to the `public/` directory must be defined - -The example below simply moves all files from the root of the project to the -`public/` directory. The `.public` workaround is so `cp` doesn't also copy -`public/` to itself in an infinite loop: - -```yaml -pages: - stage: deploy - script: - - mkdir .public - - cp -r * .public - - mv .public public - artifacts: - paths: - - public - only: - - master -``` - -Read more on [GitLab Pages user documentation](../../user/project/pages/index.md). - ## `image` and `services` This allows to specify a custom Docker image and a list of services that can be @@ -259,7 +148,7 @@ There are also two edge cases worth mentioning: 1. If no `stages` are defined in `.gitlab-ci.yml`, then the `build`, `test` and `deploy` are allowed to be used as job's stage by default. -2. If a job doesn't specify a `stage`, the job is assigned the `test` stage. +1. If a job doesn't specify a `stage`, the job is assigned the `test` stage. ## `stage` @@ -327,7 +216,7 @@ a "key: value" pair. Be careful when using special characters: jobs are created: 1. `only` defines the names of branches and tags for which the job will run. -2. `except` defines the names of branches and tags for which the job will +1. `except` defines the names of branches and tags for which the job will **not** run. There are a few rules that apply to the usage of job policy: @@ -676,9 +565,9 @@ cleanup_job: The above script will: 1. Execute `cleanup_build_job` only when `build_job` fails. -2. Always execute `cleanup_job` as the last step in pipeline regardless of +1. Always execute `cleanup_job` as the last step in pipeline regardless of success or failure. -3. Allow you to manually execute `deploy_job` from GitLab's UI. +1. Allow you to manually execute `deploy_job` from GitLab's UI. ### `when:manual` @@ -1621,7 +1510,6 @@ Possible values for `when` are: - `missing_dependency_failure`: Retry if a dependency was missing. - `runner_unsupported`: Retry if the runner was unsupported. - ## `parallel` > [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/22631) in GitLab 11.5. @@ -1644,193 +1532,213 @@ test: ## `include` -> Introduced in [GitLab Premium](https://about.gitlab.com/pricing/) 10.5. -> Available for Starter, Premium and Ultimate since 10.6. -> Behaviour expanded in GitLab 10.8 to allow more flexible overriding. -> [Moved](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/21603) -to GitLab Core in 11.4 -> In GitLab 11.7, support for [including GitLab-supplied templates directly](https://gitlab.com/gitlab-org/gitlab-ce/issues/53445) and support for [including templates from another repository](https://gitlab.com/gitlab-org/gitlab-ce/issues/53903) was added. +> - Introduced in [GitLab Premium](https://about.gitlab.com/pricing/) 10.5. +> - Available for Starter, Premium and Ultimate since 10.6. +> - [Moved](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/21603) to GitLab Core in 11.4. Using the `include` keyword, you can allow the inclusion of external YAML files. +`include` requires the external YAML file to have the extensions `.yml` or `.yaml`, +otherwise the external file will not be included. -In the following example, the content of `.before-script-template.yml` will be -automatically fetched and evaluated along with the content of `.gitlab-ci.yml`: +The files defined in `include` are: -```yaml -# Content of https://gitlab.com/awesome-project/raw/master/.before-script-template.yml +- Deep merged with those in `.gitlab-ci.yml`. +- Always evaluated first and merged with the content of `.gitlab-ci.yml`, + regardless of the position of the `include` keyword. -before_script: - - apt-get update -qq && apt-get install -y -qq sqlite3 libsqlite3-dev nodejs - - gem install bundler --no-document - - bundle install --jobs $(nproc) "${FLAGS[@]}" -``` +TIP: **Tip:** +Use merging to customize and override included CI/CD configurations with local +definitions. -```yaml -# Content of .gitlab-ci.yml +Recursive includes are not supported. Your external files should not use the +`include` keyword as it will be ignored. -include: 'https://gitlab.com/awesome-project/raw/master/.before-script-template.yml' +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 +of using YAML anchors, you can use the [`extends` keyword](#extends). -rspec: - script: - - bundle exec rspec -``` +`include` supports four include methods: -NOTE: **Note:** -`include` requires the external YAML files to have the extensions `.yml` or `.yaml`. -The external file will not be included if the extension is missing. +- [`local`](#includelocal) +- [`file`](#includefile) +- [`template`](#includetemplate) +- [`remote`](#includeremote) -You can include your extra YAML file either as a single string or -as an array of multiple values. You can also use full paths or -relative URLs. The following examples are both valid: +See [usage examples](#include-examples). -```yaml -# Single string +### `include:local` -include: '/templates/.after-script-template.yml' -``` +`include:local` includes a file from the same repository as `.gitlab-ci.yml`. +It's referenced using full paths relative to the root directory (`/`). -```yaml -# Single string +You can only use files that are currently tracked by Git on the same branch +your configuration file is on. In other words, when using a `include:local`, make +sure that both `.gitlab-ci.yml` and the local file are on the same branch. + +NOTE: **Note:** +Including local files through Git submodules paths is not supported. +Example: + +```yaml include: - file: '/templates/.after-script-template.yml' + - local: '/templates/.gitlab-ci-template.yml' ``` -```yaml -# Array +### `include:file` + +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/53903) in GitLab 11.7. + +To include files from another private project under the same GitLab instance, +use `include:file`. This file is referenced using full paths relative to the +root directory (`/`). For example: +```yaml include: - - 'https://gitlab.com/awesome-project/raw/master/.before-script-template.yml' - - '/templates/.after-script-template.yml' + - project: 'my-group/my-project' + file: '/templates/.gitlab-ci-template.yml' ``` -```yaml -# Array mixed syntax +You can also specify `ref`, with the default being the `HEAD` of the project: +```yaml include: - - 'https://gitlab.com/awesome-project/raw/master/.before-script-template.yml' - - '/templates/.after-script-template.yml' - - template: Auto-DevOps.gitlab-ci.yml + - project: 'my-group/my-project' + ref: master + file: '/templates/.gitlab-ci-template.yml' + + - project: 'my-group/my-project' + ref: v1.0.0 + file: '/templates/.gitlab-ci-template.yml' + + - project: 'my-group/my-project' + ref: 787123b47f14b552955ca2786bc9542ae66fee5b # Git SHA + file: '/templates/.gitlab-ci-template.yml' ``` -```yaml -# Array +### `include:template` + +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/53445) in GitLab 11.7. +`include:template` can be used to include `.gitlab-ci.yml` templates that are +[shipped with GitLab](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/lib/gitlab/ci/templates). + +For example: + +```yaml +# File sourced from GitLab's template collection include: - - remote: 'https://gitlab.com/awesome-project/raw/master/.before-script-template.yml' - - local: '/templates/.after-script-template.yml' - template: Auto-DevOps.gitlab-ci.yml ``` ---- +### `include:remote` + +`include:remote` can be used to include a file from a different location, +using HTTP/HTTPS, referenced by using the full URL. The remote file must be +publicly accessible through a simple GET request as authentication schemas +in the remote URL is not supported. For example: -`include` supports four types of files: +```yaml +include: + - remote: 'https://gitlab.com/awesome-project/raw/master/.gitlab-ci-template.yml' +``` -- **local** to the same repository, referenced by using full paths in the same - repository, with `/` being the root directory. For example: +NOTE: **Note for GitLab admins:** +In order to include files from another repository inside your local network, +you may need to enable the **Allow requests to the local network from hooks and services** checkbox +located in the **Admin area > Settings > Network > Outbound requests** section. - ```yaml - # Within the repository - include: '/templates/.gitlab-ci-template.yml' - ``` +### `include` examples - Or using: +Here are a few more `include` examples. - ```yaml - # Within the repository - include: - local: '/templates/.gitlab-ci-template.yml' - ``` +#### Single string or array of multiple values - NOTE: **Note:** - You can only use files that are currently tracked by Git on the same branch - your configuration file is. In other words, when using a **local file**, make - sure that both `.gitlab-ci.yml` and the local file are on the same branch. +You can include your extra YAML file(s) either as a single string or +an array of multiple values. The following examples are all valid. - NOTE: **Note:** - We don't support the inclusion of local files through Git submodules paths. +Single string with the `include:local` method implied: -- **file** from another repository, referenced by using full paths in the same - repository, with `/` being the root directory. For example: +```yaml +include: '/templates/.after-script-template.yml' +``` - ```yaml - include: - project: 'my-group/my-project' - file: '/templates/.gitlab-ci-template.yml' - ``` +Array with `include` method implied: - You can also specify `ref:`. The default `ref:` is the `HEAD` of the project: +```yaml +include: + - 'https://gitlab.com/awesome-project/raw/master/.before-script-template.yml' + - '/templates/.after-script-template.yml' +``` - ```yaml - include: - - project: 'my-group/my-project' - ref: master - file: '/templates/.gitlab-ci-template.yml' +Single string with `include` method specified explicitly: - - project: 'my-group/my-project' - ref: v1.0.0 - file: '/templates/.gitlab-ci-template.yml' +```yaml +include: + remote: 'https://gitlab.com/awesome-project/raw/master/.before-script-template.yml' +``` - - project: 'my-group/my-project' - ref: 787123b47f14b552955ca2786bc9542ae66fee5b # git sha - file: '/templates/.gitlab-ci-template.yml' - ``` +Array with `include:remote` being the single item: -- **remote** in a different location, accessed using HTTP/HTTPS, referenced - using the full URL. For example: +```yaml +include: + - remote: 'https://gitlab.com/awesome-project/raw/master/.before-script-template.yml' +``` - ```yaml - # File sourced from outside repository - include: 'https://gitlab.com/awesome-project/raw/master/.gitlab-ci-template.yml' - ``` +Array with multiple `include` methods specified explicitly: - Or using: +```yaml +include: + - remote: 'https://gitlab.com/awesome-project/raw/master/.before-script-template.yml' + - local: '/templates/.after-script-template.yml' + - template: Auto-DevOps.gitlab-ci.yml +``` - ```yaml - # File sourced from outside repository - include: - remote: 'https://gitlab.com/awesome-project/raw/master/.gitlab-ci-template.yml' - ``` +Array mixed syntax: - NOTE: **Note:** - The remote file must be publicly accessible through a simple GET request, as we don't support authentication schemas in the remote URL. +```yaml +include: + - 'https://gitlab.com/awesome-project/raw/master/.before-script-template.yml' + - '/templates/.after-script-template.yml' + - template: Auto-DevOps.gitlab-ci.yml + - project: 'my-group/my-project' + ref: master + file: '/templates/.gitlab-ci-template.yml' +``` - NOTE: **Note:** - In order to include files from another repository inside your local network, - you may need to enable the **Allow requests to the local network from hooks and services** checkbox - located in the **Settings > Network > Outbound requests** section within the **Admin area**. +#### Re-using a `before_script` template -- **template** included with GitLab. For example: +In the following example, the content of `.before-script-template.yml` will be +automatically fetched and evaluated along with the content of `.gitlab-ci.yml`. - ```yaml - # File sourced from GitLab's template collection - include: - template: Auto-DevOps.gitlab-ci.yml - ``` +Content of `https://gitlab.com/awesome-project/raw/master/.before-script-template.yml`: - NOTE: **Note:** - Templates included this way are sourced from [lib/gitlab/ci/templates](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/lib/gitlab/ci/templates). +```yaml +before_script: + - apt-get update -qq && apt-get install -y -qq sqlite3 libsqlite3-dev nodejs + - gem install bundler --no-document + - bundle install --jobs $(nproc) "${FLAGS[@]}" +``` ---- +Content of `.gitlab-ci.yml`: +```yaml +include: 'https://gitlab.com/awesome-project/raw/master/.before-script-template.yml' -Since GitLab 10.8 we are now deep merging the files defined in `include` -with those in `.gitlab-ci.yml`. Files defined by `include` are always -evaluated first and merged with the content of `.gitlab-ci.yml`, no -matter the position of the `include` keyword. You can take advantage of -merging to customize and override details in included CI -configurations with local definitions. +rspec: + script: + - bundle exec rspec +``` -NOTE: **Note:** -The recursive includes are not supported, meaning your external files -should not use the `include` keyword, as it will be ignored. +#### Overriding external template values The following example shows specific YAML-defined variables and details of the `production` job from an include file being customized in `.gitlab-ci.yml`. -```yaml -# Content of https://company.com/autodevops-template.yml +Content of `https://company.com/autodevops-template.yml`: +```yaml variables: POSTGRES_USER: user POSTGRES_PASSWORD: testing_password @@ -1848,9 +1756,9 @@ production: - master ``` -```yaml -# Content of .gitlab-ci.yml +Content of `.gitlab-ci.yml`: +```yaml include: 'https://company.com/autodevops-template.yml' image: alpine:latest @@ -1877,11 +1785,11 @@ with the environment url of the `production` job defined in The merging lets you extend and override dictionary mappings, but you cannot add or modify items to an included array. For example, to add an additional item to the production job script, you must repeat the -existing script items. +existing script items: -```yaml -# Content of https://company.com/autodevops-template.yml +Content of `https://company.com/autodevops-template.yml`: +```yaml production: stage: production script: @@ -1889,9 +1797,9 @@ production: - deploy ``` -```yaml -# Content of .gitlab-ci.yml +Content of `.gitlab-ci.yml`: +```yaml include: 'https://company.com/autodevops-template.yml' stages: @@ -1908,10 +1816,140 @@ In this case, if `install_dependencies` and `deploy` were not repeated in `.gitlab-ci.yml`, they would not be part of the script for the `production` job in the combined CI configuration. +## `extends` + +> Introduced in GitLab 11.3. + +`extends` defines an entry name that a job that uses `extends` is going to +inherit from. + +It is an alternative to using [YAML anchors](#anchors) and is a little +more flexible and readable: + +```yaml +.tests: + script: rake test + stage: test + only: + refs: + - branches + +rspec: + extends: .tests + script: rake rspec + only: + variables: + - $RSPEC +``` + +In the example above, the `rspec` job inherits from the `.tests` template job. +GitLab will perform a reverse deep merge based on the keys. GitLab will: + +- Merge the `rspec` contents into `.tests` recursively. +- Not merge the values of the keys. + +This results in the following `rspec` job: + +```yaml +rspec: + script: rake rspec + stage: test + only: + refs: + - branches + variables: + - $RSPEC +``` + NOTE: **Note:** -We currently do not support using YAML aliases across different YAML files -sourced by `include`. You must only refer to aliases in the same file. Instead -of using YAML anchors you can use [`extends` keyword](#extends). +Note that `script: rake test` has been overwritten by `script: rake rspec`. + +If you do want to include the `rake test`, see [`before_script` and `after_script`](#before_script-and-after_script). + +`.tests` in this example is a [hidden key](#hidden-keys-jobs), but it's +possible to inherit from regular jobs as well. + +`extends` supports multi-level inheritance, however it is not recommended to +use more than three levels. The maximum nesting level that is supported is 10. +The following example has two levels of inheritance: + +```yaml +.tests: + only: + - pushes + +.rspec: + extends: .tests + script: rake rspec + +rspec 1: + variables: + RSPEC_SUITE: '1' + extends: .rspec + +rspec 2: + variables: + RSPEC_SUITE: '2' + extends: .rspec + +spinach: + extends: .tests + script: rake spinach +``` + +## Using `extends` and `include` together + +`extends` works across configuration files combined with `include`. + +For example, if you have a local `included.yml` file: + +```yaml +.template: + script: + - echo Hello! +``` + +Then, in `.gitlab-ci.yml` you can use it like this: + +```yaml +include: included.yml + +useTemplate: + image: alpine + extends: .template +``` + +This will run a job called `useTemplate` that runs `echo Hello!` as defined in +the `.template` job, and uses the `alpine` Docker image as defined in the local job. + +## `pages` + +`pages` is a special job that is used to upload static content to GitLab that +can be used to serve your website. It has a special syntax, so the two +requirements below must be met: + +- Any static content must be placed under a `public/` directory. +- `artifacts` with a path to the `public/` directory must be defined. + +The example below simply moves all files from the root of the project to the +`public/` directory. The `.public` workaround is so `cp` doesn't also copy +`public/` to itself in an infinite loop: + +```yaml +pages: + stage: deploy + script: + - mkdir .public + - cp -r * .public + - mv .public public + artifacts: + paths: + - public + only: + - master +``` + +Read more on [GitLab Pages user documentation](../../user/project/pages/index.md). ## `variables` @@ -1950,9 +1988,9 @@ which can be set in GitLab's UI. ### Git strategy -> Introduced in GitLab 8.9 as an experimental feature. May change or be removed - completely in future releases. `GIT_STRATEGY=none` requires GitLab Runner - v1.7+. +> Introduced in GitLab 8.9 as an experimental feature. May change or be removed +> completely in future releases. `GIT_STRATEGY=none` requires GitLab Runner +> v1.7+. You can set the `GIT_STRATEGY` used for getting recent application code, either globally or per-job in the [`variables`](#variables) section. If left @@ -2282,8 +2320,9 @@ capitalization, the commit will be created but the pipeline will be skipped. Alternatively, one can pass the `ci.skip` [Git push option][push-option] if using Git 2.10 or newer: -``` -$ git push -o ci.skip + +```sh +git push -o ci.skip ``` ## Validate the .gitlab-ci.yml diff --git a/doc/development/README.md b/doc/development/README.md index f22dde32de9..05715274a81 100644 --- a/doc/development/README.md +++ b/doc/development/README.md @@ -47,6 +47,7 @@ description: 'Learn how to contribute to GitLab.' - [Avoid modules with instance variables](module_with_instance_variables.md) if possible - [How to dump production data to staging](db_dump.md) - [Working with the GitHub importer](github_importer.md) +- [Import/Export development documentation](import_export.md) - [Working with Merge Request diffs](diffs.md) - [Permissions](permissions.md) - [Prometheus metrics](prometheus_metrics.md) diff --git a/doc/development/contributing/issue_workflow.md b/doc/development/contributing/issue_workflow.md index 7c7da50a149..6d9149004fe 100644 --- a/doc/development/contributing/issue_workflow.md +++ b/doc/development/contributing/issue_workflow.md @@ -6,7 +6,7 @@ scheduling into milestones. Labelling is a task for everyone. Most issues will have labels for at least one of the following: -- Type: ~"feature proposal", ~bug, ~customer, etc. +- Type: ~feature, ~bug, ~customer, etc. - Subject: ~wiki, ~"container registry", ~ldap, ~api, ~frontend, etc. - Team: ~Plan, ~Manage, ~Quality, etc. - Stage: ~"devops:plan", ~"devops:create", etc. @@ -27,8 +27,8 @@ labels, you can _always_ add the team and type, and often also the subject. Type labels are very important. They define what kind of issue this is. Every issue should have one or more. -Examples of type labels are ~"feature proposal", ~bug, ~customer, ~security, -and ~"direction". +Examples of type labels are ~feature, ~bug, ~customer, ~security, +and ~direction. A number of type labels have a priority assigned to them, which automatically makes them float to the top, depending on their importance. @@ -200,7 +200,7 @@ We add the ~"Accepting merge requests" label to: - Low priority ~bug issues (i.e. we do not add it to the bugs that we want to solve in the ~"Next Patch Release") -- Small ~"feature proposal" +- Small ~feature - Small ~"technical debt" issues After adding the ~"Accepting merge requests" label, we try to estimate the @@ -259,10 +259,10 @@ For feature proposals for EE, open an issue on the [issue tracker of EE][ee-tracker]. In order to help track the feature proposals, we have created a -[`feature proposal`][fpl] label. For the time being, users that are not members +[`feature`][fl] label. For the time being, users that are not members of the project cannot add labels. You can instead ask one of the [core team] -members to add the label ~"feature proposal" to the issue or add the following -code snippet right after your description in a new line: `~"feature proposal"`. +members to add the label ~feature to the issue or add the following +code snippet right after your description in a new line: `~feature`. Please keep feature proposals as small and simple as possible, complex ones might be edited to make them small and simple. @@ -276,7 +276,7 @@ need to ask one of the [core team] members to add the label, if you do not have If you want to create something yourself, consider opening an issue first to discuss whether it is interesting to include this in GitLab. -[fpl]: https://gitlab.com/gitlab-org/gitlab-ce/issues?label_name=feature+proposal +[fl]: https://gitlab.com/gitlab-org/gitlab-ce/issues?label_name=feature ## Issue tracker guidelines diff --git a/doc/development/documentation/styleguide.md b/doc/development/documentation/styleguide.md index a18c21d921a..c188819560e 100644 --- a/doc/development/documentation/styleguide.md +++ b/doc/development/documentation/styleguide.md @@ -15,7 +15,7 @@ For programmatic help adhering to the guidelines, see [linting](index.md#linting ## Files - [Directory structure](index.md#location-and-naming-documents): place the docs -in the correct location. + in the correct location. - [Documentation files](index.md#documentation-files): name the files accordingly. DANGER: **Attention:** @@ -29,12 +29,12 @@ a test that will fail if it spots a new `README.md` file. ### Markdown The [documentation website](https://docs.gitlab.com) had its markdown engine migrated from [Redcarpet to GitLab Kramdown](https://gitlab.com/gitlab-com/gitlab-docs/merge_requests/108) -in October, 2018. +in October 2018. The [`gitlab-kramdown`](https://gitlab.com/gitlab-org/gitlab_kramdown) gem will support all [GFM markup](../../user/markdown.md) in the future. For now, use regular markdown markup, following the rules on this style guide. For a complete -Kramdown reference, check the [GiLab Markdown Kramdown Guide](https://about.gitlab.com/handbook/product/technical-writing/markdown-guide/). +Kramdown reference, check the [GitLab Markdown Kramdown Guide](https://about.gitlab.com/handbook/product/technical-writing/markdown-guide/). Use Kramdown markup wisely: do not overuse its specific markup (e.g., `{:.class}`) as it will not render properly in [`/help`](#gitlab-help). @@ -45,38 +45,38 @@ yield a useful result, and ensuring content is helpful and easy to consume. - What to include: - Any and all helpful information, processes, and tips for implementing, -using, and troubleshooting GitLab features. [The documentation is the single source of truth](https://about.gitlab.com/handbook/documentation/#documentation-as-single-source-of-truth-ssot) -for this information. + using, and troubleshooting GitLab features. [The documentation is the single source of truth](https://about.gitlab.com/handbook/documentation/#documentation-as-single-source-of-truth-ssot) + for this information. - 'Risky' or niche problem-solving steps. There is no reason to withhold these or -store them elsewhere; simply include them along with the rest of the docs including all necessary -detail, such as specific warnings and caveats about potential ramifications. + store them elsewhere; simply include them along with the rest of the docs including all necessary + detail, such as specific warnings and caveats about potential ramifications. - Any content types/sources, if relevant to users or admins. You can freely -include presentations, videos, etc.; no matter who it was originally written for, -if it is helpful to any of our audiences, we can include it. If an outside source -that's under copyright, rephrase, or summarize and link out; do not copy and paste. + include presentations, videos, etc.; no matter who it was originally written for, + if it is helpful to any of our audiences, we can include it. If an outside source + that's under copyright, rephrase, or summarize and link out; do not copy and paste. - All applicable subsections as described on the [structure and template](structure.md) page, -with files organized in the [correct directory](index.md#documentation-directory-structure). + with files organized in the [correct directory](index.md#documentation-directory-structure). - To ensure discoverability, link to each doc from its higher-level index page and other related pages. - When referencing other GitLab products and features, link to their respective docs; when referencing third-party products or technologies, link out to their external sites, documentation, and resources. - Do not duplicate information. - Structure content in alphabetical order in tables, lists, etc., unless there is -a logical reason not to (for example, when mirroring the UI or an ordered sequence). + a logical reason not to (for example, when mirroring the UI or an ordered sequence). ## Language - Use inclusive language and avoid jargon, as well as uncommon -words. The docs should be clear and easy to understand. + words. The docs should be clear and easy to understand. - Write in the 3rd person (use "we", "you", "us", "one", instead of "I" or "me"). - Be clear, concise, and stick to the goal of the doc. - Write in US English. - Capitalize "G" and "L" in GitLab. - Use title case when referring to [features](https://about.gitlab.com/features/) or -[products](https://about.gitlab.com/pricing/) (e.g., GitLab Runner, Geo, -Issue Boards, GitLab Core, Git, Prometheus, Kubernetes, etc), and methods or methodologies -(e.g., Continuous Integration, Continuous Deployment, Scrum, Agile, etc). Note that -some features are also objects (e.g. "GitLab's Merge Requests support X." and "Create a new merge request for Z."). + [products](https://about.gitlab.com/pricing/) (e.g., GitLab Runner, Geo, + Issue Boards, GitLab Core, Git, Prometheus, Kubernetes, etc), and methods or methodologies + (e.g., Continuous Integration, Continuous Deployment, Scrum, Agile, etc). Note that + some features are also objects (e.g. "GitLab's Merge Requests support X." and "Create a new merge request for Z."). ## Text @@ -127,9 +127,9 @@ Check specific punctuation rules for [list items](#list-items) below. **Markup:** -- Use dashes (`- `) for unordered lists instead of asterisks (`* `). +- Use dashes (`-`) for unordered lists instead of asterisks (`*`). - Use the number one (`1`) for each item in an ordered list. -When rendered, the list items will appear with sequential numbering. + When rendered, the list items will appear with sequential numbering. **Punctuation:** @@ -226,12 +226,11 @@ For other punctuation rules, please refer to the To indicate the steps of navigation through the UI: - - Use the exact word as shown in the UI, including any capital letters as-is. - Use bold text for navigation items and the char "greater than" (`>`) as separator -(e.g., `Navigate to your project's **Settings > CI/CD**` ). + (e.g., `Navigate to your project's **Settings > CI/CD**` ). - If there are any expandable menus, make sure to mention that the user -needs to expand the tab to find the settings you're referring to (e.g., `Navigate to your project's **Settings > CI/CD** and expand **General pipelines**`). + needs to expand the tab to find the settings you're referring to (e.g., `Navigate to your project's **Settings > CI/CD** and expand **General pipelines**`). ## Images @@ -246,7 +245,7 @@ needs to expand the tab to find the settings you're referring to (e.g., `Navigat - Compress all images with <https://tinypng.com/> or similar tool. - Compress gifs with <https://ezgif.com/optimize> or similar tool. - Images should be used (only when necessary) to _illustrate_ the description -of a process, not to _replace_ it. + of a process, not to _replace_ it. - Max image size: 100KB (gifs included). - The GitLab docs do not support videos yet. @@ -266,27 +265,33 @@ Inside the document: ## Code blocks - Always wrap code added to a sentence in inline code blocks (``` ` ```). -E.g., `.gitlab-ci.yml`, `git add .`, `CODEOWNERS`, `only: master`. -File names, commands, entries, and anything that refers to code should be added to code blocks. -To make things easier for the user, always add a full code block for things that can be -useful to copy and paste, as they can easily do it with the button on code blocks. + E.g., `.gitlab-ci.yml`, `git add .`, `CODEOWNERS`, `only: master`. + File names, commands, entries, and anything that refers to code should be added to code blocks. + To make things easier for the user, always add a full code block for things that can be + useful to copy and paste, as they can easily do it with the button on code blocks. - For regular code blocks, always use a highlighting class corresponding to the -language for better readability. Examples: + language for better readability. Examples: + + ````md + ```ruby + Ruby code + ``` - ```md - ```ruby - Ruby code - ``` + ```js + JavaScript code + ``` - ```js - JavaScript code - ``` + ```md + Markdown code + ``` - ```md - Markdown code - ``` - ``` + ```text + Code for which no specific highlighting class is available. + ``` + ```` +- To display raw markdown instead of rendered markdown, use four backticks on their own lines around the + markdown to display. See [example](https://gitlab.com/gitlab-org/gitlab-ce/blob/8c1991b9bb7e3b8d606481fdea316d633cfa5eb7/doc/development/documentation/styleguide.md#L275-287). - For a complete reference on code blocks, check the [Kramdown guide](https://about.gitlab.com/handbook/product/technical-writing/markdown-guide/#code-blocks). ## Alert boxes @@ -384,7 +389,7 @@ Which renders to: > ### This is an `h3` >{:.no_toc} -## Specific sections and terms +## Terms To maintain consistency through GitLab documentation, the following guides documentation authors on agreed styles and usage of terms. @@ -413,7 +418,7 @@ The following are recommended verbs for specific uses. |:------------|:--------------------------------|:-------------------| | "go" | making a browser go to location | "navigate", "open" | -### GitLab versions and tiers +## GitLab versions and tiers - Every piece of documentation that comes with a new feature should declare the GitLab version that feature got introduced. Right below the heading add a @@ -438,7 +443,7 @@ The following are recommended verbs for specific uses. > [Introduced](<link-to-issue>) in [GitLab Starter](https://about.gitlab.com/pricing/) 10.3. ``` -#### Early versions of EE +### Early versions of EE If the feature was created before GitLab 9.2 (before [different EE tiers were introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/1851)): @@ -451,7 +456,7 @@ For example: > [Introduced](<link-to-issue>) in GitLab Enterprise Edition 9.0. Available in [GitLab Premium](https://about.gitlab.com/pricing/). ``` -### Product badges +## Product badges When a feature is available in EE-only tiers, add the corresponding tier according to the feature availability: @@ -472,12 +477,16 @@ keyword "only": The tier should be ideally added to headers, so that the full badge will be displayed. However, it can be also mentioned from paragraphs, list items, and table cells. For these cases, the tier mention will be represented by an orange question mark that will show the tiers on hover. -E.g., `**[STARTER]**` renders **[STARTER]**, `**[STARTER ONLY]**` renders **[STARTER ONLY]**. + +For example: + +- `**[STARTER]**` renders as **[STARTER]** +- `**[STARTER ONLY]**` renders as **[STARTER ONLY]** The absence of tiers' mentions mean that the feature is available in GitLab Core, GitLab.com Free, and all higher tiers. -#### How it works +### How it works Introduced by [!244](https://gitlab.com/gitlab-com/gitlab-docs/merge_requests/244), the special markup `**[STARTER]**` will generate a `span` element to trigger the @@ -592,12 +601,12 @@ on this document. Further explanation is given below. The following can be used as a template to get started: -```md +````md ## Descriptive title One or two sentence description of what endpoint does. -``` +```text METHOD /endpoint ``` @@ -609,7 +618,7 @@ METHOD /endpoint Example request: ```sh -curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" 'https://gitlab.example.com/api/v4/endpoint?parameters' +curl --header "PRIVATE-TOKEN: <your_access_token>" 'https://gitlab.example.com/api/v4/endpoint?parameters' ``` Example response: @@ -620,8 +629,7 @@ Example response: } ] ``` - -``` +```` ### Fake tokens diff --git a/doc/development/fe_guide/index.md b/doc/development/fe_guide/index.md index ad87ecf1b87..3a3cb77f592 100644 --- a/doc/development/fe_guide/index.md +++ b/doc/development/fe_guide/index.md @@ -21,8 +21,8 @@ We also use [Axios][axios] to handle all of our network requests. We also utilize [webpack][webpack] to handle the bundling, minification, and compression of our assets. -Working with our frontend assets requires Node (v6.0 or greater) and Yarn -(v1.2 or greater). You can find information on how to install these on our +Working with our frontend assets requires Node (v8.10.0 or greater) and Yarn +(v1.10.0 or greater). You can find information on how to install these on our [installation guide][install]. ### Browser Support diff --git a/doc/development/import_export.md b/doc/development/import_export.md new file mode 100644 index 00000000000..71db1abb201 --- /dev/null +++ b/doc/development/import_export.md @@ -0,0 +1,352 @@ +# Import/Export development documentation + +Troubleshooing and general development guidelines and tips for the [Import/Export feature](../user/project/settings/import_export.md). + +<i class="fa fa-youtube-play youtube" aria-hidden="true"></i> This document is originally based on the [Import/Export 201 presentation available on YouTube](https://www.youtube.com/watch?v=V3i1OfExotE). + +## Troubleshooting commands + +Finds information about the status of the import and further logs using the JID: + +```ruby +# Rails console +Project.find_by_full_path('group/project').import_state.slice(:jid, :status, :last_error) +> {"jid"=>"414dec93f941a593ea1a6894", "status"=>"finished", "last_error"=>nil} +``` + +```bash +# Logs +grep JID /var/log/gitlab/sidekiq/current +grep "Import/Export error" /var/log/gitlab/sidekiq/current +grep "Import/Export backtrace" /var/log/gitlab/sidekiq/current +``` + +## Troubleshooting performance issues + +Read through the current performance problems using the Import/Export below. + +### OOM errors + +Out of memory (OOM) errors are normally caused by the [Sidekiq Memory Killer](https://docs.gitlab.com/ee/administration/operations/sidekiq_memory_killer.html): + +```bash +SIDEKIQ_MEMORY_KILLER_MAX_RSS = 2GB in GitLab.com +``` + +An import status `started`, and the following sidekiq logs will signal a memory issue: + +```bash +WARN: Work still in progress <struct with JID> +``` + +### Timeouts + +Timeout errors occur due to the `StuckImportJobsWorker` marking the process as failed: + +```ruby +class StuckImportJobsWorker + include ApplicationWorker + include CronjobQueue + + IMPORT_JOBS_EXPIRATION = 15.hours.to_i + + def perform + import_state_without_jid_count = mark_import_states_without_jid_as_failed! + import_state_with_jid_count = mark_import_states_with_jid_as_failed! + ... +``` + +```bash +Marked stuck import jobs as failed. JIDs: xyz +``` + +``` + +-----------+ +-----------------------------------+ + |Export Job |--->| Calls ActiveRecord `as_json` and | + +-----------+ | `to_json` on all project models | + +-----------------------------------+ + + +-----------+ +-----------------------------------+ + |Import Job |--->| Loads all JSON in memory, then | + +-----------+ | inserts into the DB in batches | + +-----------------------------------+ +``` + +### Problems and solutions + +| Problem | Possible solutions | +| -------- | -------- | +| [Slow JSON](https://gitlab.com/gitlab-org/gitlab-ce/issues/54084) loading/dumping models from the database | [split the worker](https://gitlab.com/gitlab-org/gitlab-ce/issues/54085) | +| | Batch export +| | Optimize SQL +| | Move away from `ActiveRecord` callbacks (difficult) +| High memory usage (see also some [analysis](https://gitlab.com/gitlab-org/gitlab-ce/issues/35389) | DB Commit sweet spot that uses less memory | +| | [Netflix Fast JSON API](https://github.com/Netflix/fast_jsonapi) may help | +| | Batch reading/writing to disk and any SQL + +### Temporary solutions + +While the performance problems are not tackled, there is a process to workaround +importing big projects, using a foreground import: + +[Foreground import](https://gitlab.com/gitlab-com/gl-infra/infrastructure/issues/5384) of big projects for customers. +(Using the import template in the [infrastructure tracker](https://gitlab.com/gitlab-com/gl-infra/infrastructure/)) + +## Security + +The Import/Export feature is constantly updated (adding new things to export), however +the code hasn't been refactored in a long time. We should perform a [code audit](https://gitlab.com/gitlab-org/gitlab-ce/issues/42135) +to make sure its dynamic nature does not increase the number of security concerns. + +### Security in the code + +Some of these classes provide a layer of security to the Import/Export. + +The `AttributeCleaner` removes any prohibited keys: + +```ruby +# AttributeCleaner +# Removes all `_ids` and other prohibited keys + class AttributeCleaner + ALLOWED_REFERENCES = RelationFactory::PROJECT_REFERENCES + RelationFactory::USER_REFERENCES + ['group_id'] + + def clean + @relation_hash.reject do |key, _value| + prohibited_key?(key) || !@relation_class.attribute_method?(key) || excluded_key?(key) + end.except('id') + end + + ... + +``` + +The `AttributeConfigurationSpec` checks and confirms the addition of new columns: + +```ruby +# AttributeConfigurationSpec +<<-MSG + It looks like #{relation_class}, which is exported using the project Import/Export, has new attributes: + + Please add the attribute(s) to SAFE_MODEL_ATTRIBUTES if you consider this can be exported. + Otherwise, please blacklist the attribute(s) in IMPORT_EXPORT_CONFIG by adding it to its correspondent + model in the +excluded_attributes+ section. + + SAFE_MODEL_ATTRIBUTES: #{File.expand_path(safe_attributes_file)} + IMPORT_EXPORT_CONFIG: #{Gitlab::ImportExport.config_file} +MSG +``` + +The `ModelConfigurationSpec` checks and confirms the addition of new models: + +```ruby +# ModelConfigurationSpec +<<-MSG + New model(s) <#{new_models.join(',')}> have been added, related to #{parent_model_name}, which is exported by + the Import/Export feature. + + If you think this model should be included in the export, please add it to `#{Gitlab::ImportExport.config_file}`. + + Definitely add it to `#{File.expand_path(ce_models_yml)}` + #{"or `#{File.expand_path(ee_models_yml)}` if the model/associations are EE-specific\n" if ee_models_hash.any?} + to signal that you've handled this error and to prevent it from showing up in the future. +MSG +``` + +The `ExportFileSpec` detects encrypted or sensitive columns: + +```ruby +# ExportFileSpec +<<-MSG + Found a new sensitive word <#{key_found}>, which is part of the hash #{parent.inspect} + If you think this information shouldn't get exported, please exclude the model or attribute in + IMPORT_EXPORT_CONFIG. + + Otherwise, please add the exception to +safe_list+ in CURRENT_SPEC using #{sensitive_word} as the + key and the correspondent hash or model as the value. + + Also, if the attribute is a generated unique token, please add it to RelationFactory::TOKEN_RESET_MODELS + if it needs to be reset (to prevent duplicate column problems while importing to the same instance). + + IMPORT_EXPORT_CONFIG: #{Gitlab::ImportExport.config_file} + CURRENT_SPEC: #{__FILE__} +MSG +``` + +## Versioning + +Import/Export does not use strict SemVer, since it has frequent constant changes +during a single GitLab release. It does require an update when there is a breaking change. + +```ruby +# ImportExport +module Gitlab + module ImportExport + extend self + + # For every version update, the version history in import_export.md has to be kept up to date. + VERSION = '0.2.4' +``` + +## Version history + +The [current version history](../user/project/settings/import_export.md) also displays the equivalent GitLab version +and it is useful for knowing which versions won't be compatible between them. + +| GitLab version | Import/Export version | +| ---------------- | --------------------- | +| 11.1 to current | 0.2.4 | +| 10.8 | 0.2.3 | +| 10.4 | 0.2.2 | +| ... | ... | +| 8.10.3 | 0.1.3 | +| 8.10.0 | 0.1.2 | +| 8.9.5 | 0.1.1 | +| 8.9.0 | 0.1.0 | + +### When to bump the version up + +We will have to bump the verision if we rename model/columns or perform any format +modifications in the JSON structure or the file structure of the archive file. + +We do not need to bump the version up in any of the following cases: + +- Add a new column or a model +- Remove a column or model (unless there is a DB constraint) +- Export new things (such as a new type of upload) + + +Every time we bump the version, the integration specs will fail and can be fixed with: + +```bash +bundle exec rake gitlab:import_export:bump_version +``` + +### Renaming columns or models + +This is a relatively common occurence that will require a version bump. + +There is also the _RC problem_ - GitLab.com runs an RC, prior to any customers, +meaning that we want to bump the version up in the next version (or patch release). + +For example: + +1. Add rename to `RelationRenameService` in X.Y +2. Remove it from `RelationRenameService` in X.Y + 1 +3. Bump Import/Export version in X.Y + 1 + +```ruby +module Gitlab + module ImportExport + class RelationRenameService + RENAMES = { + 'pipelines' => 'ci_pipelines' # Added in 11.6, remove in 11.7 + }.freeze +``` + +## A quick dive into the code + +### Import/Export configuration (`import_export.yml`) + +The main configuration `import_export.yml` defines what models can be exported/imported. + +Model relationships to be included in the project import/export: + +```yaml +project_tree: + - labels: + :priorities + - milestones: + - events: + - :push_event_payload + - issues: + - events: + - ... +``` + +Only include the following attributes for the models specified: + +```yaml +included_attributes: + user: + - :id + - :email + ... + +``` + +Do not include the following attributes for the models specified: + +```yaml +excluded_attributes: + project: + - :name + - :path + - ... +``` + +Extra methods to be called by the export: + +```yaml +# Methods +methods: + labels: + - :type + label: + - :type +``` + +### Import + +The import job status moves from `none` to `finished` or `failed` into different states: + +_import\_status_: none -> scheduled -> started -> finished/failed + +While the status is `started` the `Importer` code processes each step required for the import. + +```ruby +# ImportExport::Importer +module Gitlab + module ImportExport + class Importer + def execute + if import_file && check_version! && restorers.all?(&:restore) && overwrite_project + project_tree.restored_project + else + raise Projects::ImportService::Error.new(@shared.errors.join(', ')) + end + rescue => e + raise Projects::ImportService::Error.new(e.message) + ensure + remove_import_file + end + + def restorers + [repo_restorer, wiki_restorer, project_tree, avatar_restorer, + uploads_restorer, lfs_restorer, statistics_restorer] + end +``` + +The export service, is similar to the `Importer`, restoring data instead of saving it. + +### Export + +```ruby +# ImportExport::ExportService +module Projects + module ImportExport + class ExportService < BaseService + + def save_all! + if save_services + Gitlab::ImportExport::Saver.save(project: project, shared: @shared) + notify_success + else + cleanup_and_notify_error! + end + end + + def save_services + [version_saver, avatar_saver, project_tree_saver, uploads_saver, repo_saver, + wiki_repo_saver, lfs_saver].all?(&:save) + end +``` diff --git a/doc/development/rake_tasks.md b/doc/development/rake_tasks.md index 2ad748d4802..ae9bf863419 100644 --- a/doc/development/rake_tasks.md +++ b/doc/development/rake_tasks.md @@ -38,6 +38,14 @@ Note that since you can't see the questions from stdout, you might just want to `echo 'yes'` to keep it running. It would still print the errors on stderr so no worries about missing errors. +### Extra Project seed options + +There are a few environment flags you can pass to change how projects are seeded + +- `SIZE`: defaults to `8`, max: `32`. Amount of projects to create. +- `LARGE_PROJECTS`: defaults to false. If set will clone 6 large projects to help with testing. +- `FORK`: defaults to false. If set to `true` will fork `torvalds/linux` five times. Can also be set to an existing project full_path and it will fork that instead. + ### Notes for MySQL Since the seeds would contain various UTF-8 characters, such as emojis or so, diff --git a/doc/development/testing_guide/flaky_tests.md b/doc/development/testing_guide/flaky_tests.md index bbb2313ea7b..f6bc9fb0979 100644 --- a/doc/development/testing_guide/flaky_tests.md +++ b/doc/development/testing_guide/flaky_tests.md @@ -5,6 +5,18 @@ It's a test that sometimes fails, but if you retry it enough times, it passes, eventually. +## Quarantined tests + +Tests can be put in quarantine by assigning `:quarantine` metadata. This means +they will be skipped unless run with `--tag quarantine`. This can be used for +tests that are expected to fail while a fix is in progress (similar to how +[`skip` or `pending`](https://relishapp.com/rspec/rspec-core/v/3-8/docs/pending-and-skipped-examples) + can be used). + +``` +bin/rspec --tag quarantine +``` + ## Automatic retries and flaky tests detection On our CI, we use [rspec-retry] to automatically retry a failing example a few diff --git a/doc/install/installation.md b/doc/install/installation.md index 5653c59f576..2eba2cc4847 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -50,62 +50,85 @@ The GitLab installation consists of setting up the following components: `sudo` is not installed on Debian by default. Make sure your system is up-to-date and install it. - # run as root! - apt-get update -y - apt-get upgrade -y - apt-get install sudo -y +```sh +# run as root! +apt-get update -y +apt-get upgrade -y +apt-get install sudo -y +``` **Note:** During this installation some files will need to be edited manually. If you are familiar with vim set it as default editor with the commands below. If you are not familiar with vim please skip this and keep using the default editor. - # Install vim and set as default editor - sudo apt-get install -y vim - sudo update-alternatives --set editor /usr/bin/vim.basic +```sh +# Install vim and set as default editor +sudo apt-get install -y vim +sudo update-alternatives --set editor /usr/bin/vim.basic +``` Install the required packages (needed to compile Ruby and native extensions to Ruby gems): - sudo apt-get install -y build-essential zlib1g-dev libyaml-dev libssl-dev libgdbm-dev libre2-dev libreadline-dev libncurses5-dev libffi-dev curl openssh-server checkinstall libxml2-dev libxslt-dev libcurl4-openssl-dev libicu-dev logrotate rsync python-docutils pkg-config cmake +```sh +sudo apt-get install -y build-essential zlib1g-dev libyaml-dev libssl-dev libgdbm-dev libre2-dev \ + libreadline-dev libncurses5-dev libffi-dev curl openssh-server checkinstall libxml2-dev \ + libxslt-dev libcurl4-openssl-dev libicu-dev logrotate rsync python-docutils pkg-config cmake +``` Ubuntu 14.04 (Trusty Tahr) doesn't have the `libre2-dev` package available, but you can [install re2 manually](https://github.com/google/re2/wiki/Install). If you want to use Kerberos for user authentication, then install libkrb5-dev: - sudo apt-get install libkrb5-dev +```sh +sudo apt-get install libkrb5-dev +``` **Note:** If you don't know what Kerberos is, you can assume you don't need it. Make sure you have the right version of Git installed - # Install Git - sudo apt-get install -y git-core +```sh +# Install Git +sudo apt-get install -y git-core - # Make sure Git is version 2.18.0 or higher - git --version +# Make sure Git is version 2.18.0 or higher +git --version +``` Is the system packaged Git too old? Remove it and compile from source. - # Remove packaged Git - sudo apt-get remove git-core +```sh +# Remove packaged Git +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 from source +cd /tmp +curl --remote-name --progress https://www.kernel.org/pub/software/scm/git/git-2.18.0.tar.gz +echo '94faf2c0b02a7920b0b46f4961d8e9cad08e81418614102898a55f980fa3e7e4 git-2.18.0.tar.gz' | shasum -a256 -c - && tar -xzf git-2.18.0.tar.gz +cd git-2.18.0/ +./configure +make prefix=/usr/local all - # Install dependencies - sudo apt-get install -y libcurl4-openssl-dev libexpat1-dev gettext libz-dev libssl-dev build-essential +# Install into /usr/local/bin +sudo make prefix=/usr/local install - # Download and compile from source - cd /tmp - curl --remote-name --progress https://www.kernel.org/pub/software/scm/git/git-2.18.0.tar.gz - echo '94faf2c0b02a7920b0b46f4961d8e9cad08e81418614102898a55f980fa3e7e4 git-2.18.0.tar.gz' | shasum -a256 -c - && tar -xzf git-2.18.0.tar.gz - cd git-2.18.0/ - ./configure - make prefix=/usr/local all +# When editing config/gitlab.yml (Step 5), change the git -> bin_path to /usr/local/bin/git +``` - # Install into /usr/local/bin - sudo make prefix=/usr/local install +For the [Custom Favicon](../customization/favicon.md) to work, graphicsmagick +needs to be installed. - # When editing config/gitlab.yml (Step 5), change the git -> bin_path to /usr/local/bin/git +```sh +sudo apt-get install -y graphicsmagick +``` **Note:** In order to receive mail notifications, make sure to install a mail server. By default, Debian is shipped with exim4 but this [has problems](https://gitlab.com/gitlab-org/gitlab-ce/issues/12754) while Ubuntu does not ship with one. The recommended mail server is postfix and you can install it with: - sudo apt-get install -y postfix +```sh +sudo apt-get install -y postfix +``` Then select 'Internet Site' and press enter to confirm the hostname. @@ -127,22 +150,28 @@ instructions are designed to install Ruby from the official source code. Remove the old Ruby 1.8 if present: - sudo apt-get remove ruby1.8 +```sh +sudo apt-get remove ruby1.8 +``` Download Ruby and compile it: - 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 - - ./configure --disable-install-rdoc - make - sudo make install +```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 + +./configure --disable-install-rdoc +make +sudo make install +``` Then install the Bundler Gem: - sudo gem install bundler --no-document +```sh +sudo gem install bundler --no-document +``` ## 3. Go @@ -151,31 +180,35 @@ GitLab we need a Go compiler. The instructions below assume you use 64-bit Linux. You can find downloads for other platforms at the [Go download page](https://golang.org/dl). - # Remove former Go installation folder - sudo rm -rf /usr/local/go - - curl --remote-name --progress https://dl.google.com/go/go1.10.3.linux-amd64.tar.gz - echo 'fa1b0e45d3b647c252f51f5e1204aba049cde4af177ef9f2181f43004f901035 go1.10.3.linux-amd64.tar.gz' | shasum -a256 -c - && \ - sudo tar -C /usr/local -xzf go1.10.3.linux-amd64.tar.gz - sudo ln -sf /usr/local/go/bin/{go,godoc,gofmt} /usr/local/bin/ - rm go1.10.3.linux-amd64.tar.gz +```sh +# Remove former Go installation folder +sudo rm -rf /usr/local/go + +curl --remote-name --progress https://dl.google.com/go/go1.10.3.linux-amd64.tar.gz +echo 'fa1b0e45d3b647c252f51f5e1204aba049cde4af177ef9f2181f43004f901035 go1.10.3.linux-amd64.tar.gz' | shasum -a256 -c - && \ + sudo tar -C /usr/local -xzf go1.10.3.linux-amd64.tar.gz +sudo ln -sf /usr/local/go/bin/{go,godoc,gofmt} /usr/local/bin/ +rm go1.10.3.linux-amd64.tar.gz +``` ## 4. Node Since GitLab 8.17, GitLab requires the use of Node to compile javascript assets, and Yarn to manage javascript dependencies. The current minimum -requirements for these are node >= v6.0.0 and yarn >= v1.2.0. In many distros +requirements for these are node >= v8.10.0 and yarn >= v1.10.0. In many distros the versions provided by the official package repositories are out of date, so we'll need to install through the following commands: - # install node v8.x - curl --location https://deb.nodesource.com/setup_8.x | sudo bash - - sudo apt-get install -y nodejs - - curl --silent --show-error https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add - - echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list - sudo apt-get update - sudo apt-get install yarn +```sh +# install node v8.x +curl --location https://deb.nodesource.com/setup_8.x | sudo bash - +sudo apt-get install -y nodejs + +curl --silent --show-error https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add - +echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list +sudo apt-get update +sudo apt-get install yarn +``` Visit the official websites for [node](https://nodejs.org/en/download/package-manager/) and [yarn](https://yarnpkg.com/en/docs/install/) if you have any trouble with these steps. @@ -183,7 +216,9 @@ Visit the official websites for [node](https://nodejs.org/en/download/package-ma Create a `git` user for GitLab: - sudo adduser --disabled-login --gecos 'GitLab' git +```sh +sudo adduser --disabled-login --gecos 'GitLab' git +``` ## 6. Database @@ -195,37 +230,37 @@ you need at least PostgreSQL 9.2. 1. Install the database packages: - ```bash + ```sh sudo apt-get install -y postgresql postgresql-client libpq-dev postgresql-contrib ``` 1. Create a database user for GitLab: - ```bash + ```sh sudo -u postgres psql -d template1 -c "CREATE USER git CREATEDB;" ``` 1. Create the `pg_trgm` extension (required for GitLab 8.6+): - ```bash + ```sh sudo -u postgres psql -d template1 -c "CREATE EXTENSION IF NOT EXISTS pg_trgm;" ``` 1. Create the GitLab production database and grant all privileges on database: - ```bash + ```sh sudo -u postgres psql -d template1 -c "CREATE DATABASE gitlabhq_production OWNER git;" ``` 1. Try connecting to the new database with the new user: - ```bash + ```sh sudo -u git -H psql -d gitlabhq_production ``` 1. Check if the `pg_trgm` extension is enabled: - ```bash + ```sh SELECT true AS enabled FROM pg_available_extensions WHERE name = 'pg_trgm' @@ -243,7 +278,7 @@ you need at least PostgreSQL 9.2. 1. Quit the database session: - ```bash + ```sh gitlabhq_production> \q ``` @@ -262,7 +297,7 @@ If you are using Debian 7 or Ubuntu 12.04, follow the special documentation on [an alternate Redis installation](redis.md). Once done, follow the rest of the guide here. -``` +```sh # Configure redis to use sockets sudo cp /etc/redis/redis.conf /etc/redis/redis.conf.orig @@ -294,89 +329,95 @@ sudo usermod -aG redis git ## 8. GitLab - # We'll install GitLab into home directory of the user "git" - cd /home/git +```sh +# We'll install GitLab into home directory of the user "git" +cd /home/git +``` ### Clone the Source - # Clone GitLab repository - sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 11-7-stable gitlab +```sh +# Clone GitLab repository +sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 11-7-stable gitlab +``` **Note:** You can change `11-7-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server! ### Configure It - # Go to GitLab installation folder - cd /home/git/gitlab +```sh +# Go to GitLab installation folder +cd /home/git/gitlab - # Copy the example GitLab config - sudo -u git -H cp config/gitlab.yml.example config/gitlab.yml +# Copy the example GitLab config +sudo -u git -H cp config/gitlab.yml.example config/gitlab.yml - # Update GitLab config file, follow the directions at top of file - sudo -u git -H editor config/gitlab.yml +# Update GitLab config file, follow the directions at top of file +sudo -u git -H editor config/gitlab.yml - # Copy the example secrets file - sudo -u git -H cp config/secrets.yml.example config/secrets.yml - sudo -u git -H chmod 0600 config/secrets.yml +# Copy the example secrets file +sudo -u git -H cp config/secrets.yml.example config/secrets.yml +sudo -u git -H chmod 0600 config/secrets.yml - # Make sure GitLab can write to the log/ and tmp/ directories - sudo chown -R git log/ - sudo chown -R git tmp/ - sudo chmod -R u+rwX,go-w log/ - sudo chmod -R u+rwX tmp/ +# Make sure GitLab can write to the log/ and tmp/ directories +sudo chown -R git log/ +sudo chown -R git tmp/ +sudo chmod -R u+rwX,go-w log/ +sudo chmod -R u+rwX tmp/ - # Make sure GitLab can write to the tmp/pids/ and tmp/sockets/ directories - sudo chmod -R u+rwX tmp/pids/ - sudo chmod -R u+rwX tmp/sockets/ +# Make sure GitLab can write to the tmp/pids/ and tmp/sockets/ directories +sudo chmod -R u+rwX tmp/pids/ +sudo chmod -R u+rwX tmp/sockets/ - # Create the public/uploads/ directory - sudo -u git -H mkdir public/uploads/ +# Create the public/uploads/ directory +sudo -u git -H mkdir public/uploads/ - # Make sure only the GitLab user has access to the public/uploads/ directory - # now that files in public/uploads are served by gitlab-workhorse - sudo chmod 0700 public/uploads +# Make sure only the GitLab user has access to the public/uploads/ directory +# now that files in public/uploads are served by gitlab-workhorse +sudo chmod 0700 public/uploads - # Change the permissions of the directory where CI job traces are stored - sudo chmod -R u+rwX builds/ +# Change the permissions of the directory where CI job traces are stored +sudo chmod -R u+rwX builds/ - # Change the permissions of the directory where CI artifacts are stored - sudo chmod -R u+rwX shared/artifacts/ +# Change the permissions of the directory where CI artifacts are stored +sudo chmod -R u+rwX shared/artifacts/ - # Change the permissions of the directory where GitLab Pages are stored - sudo chmod -R ug+rwX shared/pages/ +# Change the permissions of the directory where GitLab Pages are stored +sudo chmod -R ug+rwX shared/pages/ - # Copy the example Unicorn config - sudo -u git -H cp config/unicorn.rb.example config/unicorn.rb +# Copy the example Unicorn config +sudo -u git -H cp config/unicorn.rb.example config/unicorn.rb - # Find number of cores - nproc +# Find number of cores +nproc - # Enable cluster mode if you expect to have a high load instance - # Set the number of workers to at least the number of cores - # Ex. change amount of workers to 3 for 2GB RAM server - sudo -u git -H editor config/unicorn.rb +# Enable cluster mode if you expect to have a high load instance +# Set the number of workers to at least the number of cores +# Ex. change amount of workers to 3 for 2GB RAM server +sudo -u git -H editor config/unicorn.rb - # Copy the example Rack attack config - sudo -u git -H cp config/initializers/rack_attack.rb.example config/initializers/rack_attack.rb +# Copy the example Rack attack config +sudo -u git -H cp config/initializers/rack_attack.rb.example config/initializers/rack_attack.rb - # Configure Git global settings for git user - # 'autocrlf' is needed for the web editor - sudo -u git -H git config --global core.autocrlf input +# Configure Git global settings for git user +# 'autocrlf' is needed for the web editor +sudo -u git -H git config --global core.autocrlf input - # Disable 'git gc --auto' because GitLab already runs 'git gc' when needed - sudo -u git -H git config --global gc.auto 0 +# Disable 'git gc --auto' because GitLab already runs 'git gc' when needed +sudo -u git -H git config --global gc.auto 0 - # Enable packfile bitmaps - sudo -u git -H git config --global repack.writeBitmaps true +# Enable packfile bitmaps +sudo -u git -H git config --global repack.writeBitmaps true - # Enable push options - sudo -u git -H git config --global receive.advertisePushOptions true +# Enable push options +sudo -u git -H git config --global receive.advertisePushOptions true - # Configure Redis connection settings - sudo -u git -H cp config/resque.yml.example config/resque.yml +# Configure Redis connection settings +sudo -u git -H cp config/resque.yml.example config/resque.yml - # Change the Redis socket path if you are not using the default Debian / Ubuntu configuration - sudo -u git -H editor config/resque.yml +# Change the Redis socket path if you are not using the default Debian / Ubuntu configuration +sudo -u git -H editor config/resque.yml +``` **Important Note:** Make sure to edit both `gitlab.yml` and `unicorn.rb` to match your setup. @@ -384,33 +425,37 @@ sudo usermod -aG redis git ### Configure GitLab DB Settings - # PostgreSQL only: - sudo -u git cp config/database.yml.postgresql config/database.yml - - # MySQL only: - sudo -u git cp config/database.yml.mysql 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). - # If you followed the database guide then please do as follows: - # Change 'secure password' with the value you have given to $password - # You can keep the double quotes around the password - sudo -u git -H editor config/database.yml - - # PostgreSQL and MySQL: - # Make config/database.yml readable to git only - sudo -u git -H chmod o-rwx config/database.yml +```sh +# PostgreSQL only: +sudo -u git cp config/database.yml.postgresql config/database.yml + +# MySQL only: +sudo -u git cp config/database.yml.mysql 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). +# If you followed the database guide then please do as follows: +# Change 'secure password' with the value you have given to $password +# You can keep the double quotes around the password +sudo -u git -H editor config/database.yml + +# PostgreSQL and MySQL: +# Make config/database.yml readable to git only +sudo -u git -H chmod o-rwx config/database.yml +``` ### Install Gems **Note:** As of bundler 1.5.2, you can invoke `bundle install -jN` (where `N` the number of your processor cores) and enjoy the parallel gems installation with measurable difference in completion time (~60% faster). Check the number of your cores with `nproc`. For more information check this [post](https://robots.thoughtbot.com/parallel-gem-installing-using-bundler). First make sure you have bundler >= 1.5.2 (run `bundle -v`) as it addresses some [issues](https://devcenter.heroku.com/changelog-items/411) that were [fixed](https://github.com/bundler/bundler/pull/2817) in 1.5.2. - # For PostgreSQL (note, the option says "without ... mysql") - sudo -u git -H bundle install --deployment --without development test mysql aws kerberos +```sh +# For PostgreSQL (note, the option says "without ... mysql") +sudo -u git -H bundle install --deployment --without development test mysql aws kerberos - # Or if you use MySQL (note, the option says "without ... postgres") - sudo -u git -H bundle install --deployment --without development test postgres aws kerberos +# Or if you use MySQL (note, the option says "without ... postgres") +sudo -u git -H bundle install --deployment --without development test postgres aws kerberos +``` **Note:** If you want to use Kerberos for user authentication, then omit `kerberos` in the `--without` option above. @@ -418,12 +463,14 @@ sudo usermod -aG redis git GitLab Shell is an SSH access and repository management software developed specially for GitLab. - # Run the installation task for gitlab-shell (replace `REDIS_URL` if needed): - sudo -u git -H bundle exec rake gitlab:shell:install REDIS_URL=unix:/var/run/redis/redis.sock RAILS_ENV=production SKIP_STORAGE_VALIDATION=true +```sh +# Run the installation task for gitlab-shell (replace `REDIS_URL` if needed): +sudo -u git -H bundle exec rake gitlab:shell:install REDIS_URL=unix:/var/run/redis/redis.sock RAILS_ENV=production SKIP_STORAGE_VALIDATION=true - # By default, the gitlab-shell config is generated from your main GitLab config. - # You can review (and modify) the gitlab-shell config as follows: - sudo -u git -H editor /home/git/gitlab-shell/config.yml +# By default, the gitlab-shell config is generated from your main GitLab config. +# You can review (and modify) the gitlab-shell config as follows: +sudo -u git -H editor /home/git/gitlab-shell/config.yml +``` **Note:** If you want to use HTTPS, see [Using HTTPS](#using-https) for the additional steps. @@ -441,57 +488,73 @@ GitLab-Workhorse uses [GNU Make](https://www.gnu.org/software/make/). The following command-line will install GitLab-Workhorse in `/home/git/gitlab-workhorse` which is the recommended location. - sudo -u git -H bundle exec rake "gitlab:workhorse:install[/home/git/gitlab-workhorse]" RAILS_ENV=production +```sh +sudo -u git -H bundle exec rake "gitlab:workhorse:install[/home/git/gitlab-workhorse]" RAILS_ENV=production +``` You can specify a different Git repository by providing it as an extra parameter: - sudo -u git -H bundle exec rake "gitlab:workhorse:install[/home/git/gitlab-workhorse,https://example.com/gitlab-workhorse.git]" RAILS_ENV=production +```sh +sudo -u git -H bundle exec rake "gitlab:workhorse:install[/home/git/gitlab-workhorse,https://example.com/gitlab-workhorse.git]" RAILS_ENV=production +``` ### Install gitlab-pages GitLab-Pages uses [GNU Make](https://www.gnu.org/software/make/). This step is optional and only needed if you wish to host static sites from within GitLab. The following commands will install GitLab-Pages in `/home/git/gitlab-pages`. For additional setup steps, please consult the [administration guide](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/administration/pages/source.md) for your version of GitLab as the GitLab Pages daemon can be ran several different ways. - cd /home/git - sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-pages.git - cd gitlab-pages - sudo -u git -H git checkout v$(</home/git/gitlab/GITLAB_PAGES_VERSION) - sudo -u git -H make +```sh +cd /home/git +sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-pages.git +cd gitlab-pages +sudo -u git -H git checkout v$(</home/git/gitlab/GITLAB_PAGES_VERSION) +sudo -u git -H make +``` ### Install Gitaly - # Fetch Gitaly source with Git and compile with Go - sudo -u git -H bundle exec rake "gitlab:gitaly:install[/home/git/gitaly,/home/git/repositories]" RAILS_ENV=production +```sh +# Fetch Gitaly source with Git and compile with Go +sudo -u git -H bundle exec rake "gitlab:gitaly:install[/home/git/gitaly,/home/git/repositories]" RAILS_ENV=production +``` You can specify a different Git repository by providing it as an extra parameter: - sudo -u git -H bundle exec rake "gitlab:gitaly:install[/home/git/gitaly,/home/git/repositories,https://example.com/gitaly.git]" RAILS_ENV=production +```sh +sudo -u git -H bundle exec rake "gitlab:gitaly:install[/home/git/gitaly,/home/git/repositories,https://example.com/gitaly.git]" RAILS_ENV=production +``` Next, make sure gitaly configured: - # Restrict Gitaly socket access - sudo chmod 0700 /home/git/gitlab/tmp/sockets/private - sudo chown git /home/git/gitlab/tmp/sockets/private +```sh +# Restrict Gitaly socket access +sudo chmod 0700 /home/git/gitlab/tmp/sockets/private +sudo chown git /home/git/gitlab/tmp/sockets/private - # If you are using non-default settings you need to update config.toml - cd /home/git/gitaly - sudo -u git -H editor config.toml +# If you are using non-default settings you need to update config.toml +cd /home/git/gitaly +sudo -u git -H editor config.toml +``` For more information about configuring Gitaly see [doc/administration/gitaly](../administration/gitaly). ### Initialize Database and Activate Advanced Features - sudo -u git -H bundle exec rake gitlab:setup RAILS_ENV=production - # Type 'yes' to create the database tables. +```sh +sudo -u git -H bundle exec rake gitlab:setup RAILS_ENV=production +# Type 'yes' to create the database tables. - # or you can skip the question by adding force=yes - sudo -u git -H bundle exec rake gitlab:setup RAILS_ENV=production force=yes +# or you can skip the question by adding force=yes +sudo -u git -H bundle exec rake gitlab:setup RAILS_ENV=production force=yes - # When done you see 'Administrator account created:' +# When done you see 'Administrator account created:' +``` **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) please wait with exposing 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. - sudo -u git -H bundle exec rake gitlab:setup RAILS_ENV=production GITLAB_ROOT_PASSWORD=yourpassword GITLAB_ROOT_EMAIL=youremail +```sh +sudo -u git -H bundle exec rake gitlab:setup RAILS_ENV=production GITLAB_ROOT_PASSWORD=yourpassword GITLAB_ROOT_EMAIL=youremail +``` ### Secure secrets.yml @@ -503,43 +566,58 @@ Otherwise your secrets are exposed if one of your backups is compromised. Download the init script (will be `/etc/init.d/gitlab`): - sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab +```sh +sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab +``` And if you are installing with a non-default folder or user copy and edit the defaults file: - sudo cp lib/support/init.d/gitlab.default.example /etc/default/gitlab +```sh +sudo cp lib/support/init.d/gitlab.default.example /etc/default/gitlab +``` If you installed GitLab in another directory or as a user other than the default you should change these settings in `/etc/default/gitlab`. Do not edit `/etc/init.d/gitlab` as it will be changed on upgrade. Make GitLab start on boot: - sudo update-rc.d gitlab defaults 21 +```sh +sudo update-rc.d gitlab defaults 21 +``` ### Set up Logrotate - sudo cp lib/support/logrotate/gitlab /etc/logrotate.d/gitlab +```sh +sudo cp lib/support/logrotate/gitlab /etc/logrotate.d/gitlab +``` ### Check Application Status Check if GitLab and its environment are configured correctly: - sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production - +```sh +sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production +``` ### Compile GetText PO files - sudo -u git -H bundle exec rake gettext:compile RAILS_ENV=production +```sh +sudo -u git -H bundle exec rake gettext:compile RAILS_ENV=production +``` ### Compile Assets - 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 +```sh +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 +``` ### Start Your GitLab Instance - sudo service gitlab start - # or - sudo /etc/init.d/gitlab restart +```sh +sudo service gitlab start +# or +sudo /etc/init.d/gitlab restart +``` ## 9. Nginx @@ -547,27 +625,33 @@ Check if GitLab and its environment are configured correctly: ### Installation - sudo apt-get install -y nginx +```sh +sudo apt-get install -y nginx +``` ### Site Configuration Copy the example site config: - sudo cp lib/support/nginx/gitlab /etc/nginx/sites-available/gitlab - sudo ln -s /etc/nginx/sites-available/gitlab /etc/nginx/sites-enabled/gitlab +```sh +sudo cp lib/support/nginx/gitlab /etc/nginx/sites-available/gitlab +sudo ln -s /etc/nginx/sites-available/gitlab /etc/nginx/sites-enabled/gitlab +``` Make sure to edit the config file to match your setup. Also, ensure that you match your paths to GitLab, especially if installing for a user other than the 'git' user: - # Change YOUR_SERVER_FQDN to the fully-qualified - # domain name of your host serving GitLab. - # - # Remember to match your paths to GitLab, especially - # if installing for a user other than 'git'. - # - # If using Ubuntu default nginx install: - # either remove the default_server from the listen line - # or else sudo rm -f /etc/nginx/sites-enabled/default - sudo editor /etc/nginx/sites-available/gitlab +```sh +# Change YOUR_SERVER_FQDN to the fully-qualified +# domain name of your host serving GitLab. +# +# Remember to match your paths to GitLab, especially +# if installing for a user other than 'git'. +# +# If using Ubuntu default nginx install: +# either remove the default_server from the listen line +# or else sudo rm -f /etc/nginx/sites-enabled/default +sudo editor /etc/nginx/sites-available/gitlab +``` If you intend to enable GitLab pages, there is a separate Nginx config you need to use. Read all about the needed configuration at the @@ -579,13 +663,17 @@ to use. Read all about the needed configuration at the Validate your `gitlab` or `gitlab-ssl` Nginx config file with the following command: - sudo nginx -t +```sh +sudo nginx -t +``` You should receive `syntax is okay` and `test is successful` messages. If you receive errors check your `gitlab` or `gitlab-ssl` Nginx config file for typos, etc. as indicated in the error message given. ### Restart - sudo service nginx restart +```sh +sudo service nginx restart +``` ## Done! @@ -593,7 +681,9 @@ You should receive `syntax is okay` and `test is successful` messages. If you re To make sure you didn't miss anything run a more thorough check with: - sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production +```sh +sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production +``` If all items are green, then congratulations on successfully installing GitLab! @@ -680,31 +770,39 @@ for the changes to take effect. If you'd like to connect to a Redis server on a non-standard port or on a different host, you can configure its connection string via the `config/resque.yml` file. - # example - production: - url: redis://redis.example.tld:6379 +``` +# example +production: + url: redis://redis.example.tld:6379 +``` If you want to connect the Redis server via socket, then use the "unix:" URL scheme and the path to the Redis socket file in the `config/resque.yml` file. - # example - production: - url: unix:/path/to/redis/socket +``` +# example +production: + url: unix:/path/to/redis/socket +``` Also you can use environment variables in the `config/resque.yml` file: - # example - production: - url: <%= ENV.fetch('GITLAB_REDIS_URL') %> +``` +# example +production: + url: <%= ENV.fetch('GITLAB_REDIS_URL') %> +``` ### Custom SSH Connection If you are running SSH on a non-standard port, you must change the GitLab user's SSH config. - # Add to /home/git/.ssh/config - host localhost # Give your setup a name (here: override localhost) - user git # Your remote git user - port 2222 # Your port number - hostname 127.0.0.1; # Your server name or IP +``` +# Add to /home/git/.ssh/config +host localhost # Give your setup a name (here: override localhost) + user git # Your remote git user + port 2222 # Your port number + hostname 127.0.0.1; # Your server name or IP +``` You also need to change the corresponding options (e.g. `ssh_user`, `ssh_host`, `admin_uri`) in the `config\gitlab.yml` file. diff --git a/doc/integration/oauth_provider.md b/doc/integration/oauth_provider.md index acc9db15826..c02a29dffb4 100644 --- a/doc/integration/oauth_provider.md +++ b/doc/integration/oauth_provider.md @@ -3,8 +3,11 @@ This document is about using GitLab as an OAuth authentication service provider to sign in to other services. -If you want to use other OAuth authentication service providers to sign in to -GitLab, please see the [OAuth2 client documentation](../api/oauth2.md). +If you want to use: + +- Other OAuth authentication service providers to sign in to + GitLab, see the [OAuth2 client documentation](omniauth.md). +- The related API, see [Applications API](../api/applications.md). ## Introduction to OAuth @@ -28,7 +31,7 @@ GitLab supports two ways of adding a new OAuth2 application to an instance. You can either add an application as a regular user or add it in the admin area. What this means is that GitLab can actually have instance-wide and a user-wide applications. There is no difference between them except for the different -permission levels they are set (user/admin). The default callback URL is +permission levels they are set (user/admin). The default callback URL is `http://your-gitlab.example.com/users/auth/gitlab/callback` ## Adding an application through the profile diff --git a/doc/raketasks/backup_restore.md b/doc/raketasks/backup_restore.md index 57bc71d2903..bb28ca35a26 100644 --- a/doc/raketasks/backup_restore.md +++ b/doc/raketasks/backup_restore.md @@ -311,6 +311,11 @@ For installations from source: remote_directory: 'my.s3.bucket' # Turns on AWS Server-Side Encryption with Amazon S3-Managed Keys for backups, this is optional # encryption: 'AES256' + # Turns on AWS Server-Side Encryption with Amazon Customer-Provided Encryption Keys for backups, this is optional + # This should be set to the base64-encoded encryption key for Amazon S3 to use to encrypt or decrypt your data. + # 'encryption' must also be set in order for this to have any effect. + # To avoid storing the key on disk, the key can also be specified via the `GITLAB_BACKUP_ENCRYPTION_KEY` environment variable. + # encryption_key: '<base64 key>' # Specifies Amazon S3 storage class to use for backups, this is optional # storage_class: 'STANDARD' ``` diff --git a/doc/security/webhooks.md b/doc/security/webhooks.md index b17b0a4bc4a..fb2b6768f0a 100644 --- a/doc/security/webhooks.md +++ b/doc/security/webhooks.md @@ -12,7 +12,7 @@ If a web service does not require authentication, Webhooks can be used to trigge To prevent this type of exploitation from happening, starting with GitLab 10.6, all Webhook requests to the current GitLab instance server address and/or in a private network will be forbidden by default. That means that all requests made to 127.0.0.1, ::1 and 0.0.0.0, as well as IPv4 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16 and IPv6 site-local (ffc0::/10) addresses won't be allowed. -This behavior can be overridden by enabling the option *"Allow requests to the local network from hooks and services"* in the *"Outbound requests"* section inside the Admin area under **Settings** (`/admin/application_settings`): +This behavior can be overridden by enabling the option *"Allow requests to the local network from hooks and services"* in the *"Outbound requests"* section inside the Admin area under **Settings** (`/admin/application_settings/network`): ![Outbound requests admin settings](img/outbound_requests_section.png) diff --git a/doc/topics/autodevops/index.md b/doc/topics/autodevops/index.md index 780e9b8783e..68e50a61151 100644 --- a/doc/topics/autodevops/index.md +++ b/doc/topics/autodevops/index.md @@ -683,6 +683,8 @@ also be customized, and you can easily use a [custom buildpack](#custom-buildpac | `PRODUCTION_REPLICAS` | The number of replicas to deploy in the production environment. This takes precedence over `REPLICAS`; defaults to 1. | | `CANARY_REPLICAS` | The number of canary replicas to deploy for [Canary Deployments](https://docs.gitlab.com/ee/user/project/canary_deployments.html); defaults to 1 | | `CANARY_PRODUCTION_REPLICAS` | The number of canary replicas to deploy for [Canary Deployments](https://docs.gitlab.com/ee/user/project/canary_deployments.html) in the production environment. This takes precedence over `CANARY_REPLICAS`; defaults to 1 | +| `ADDITIONAL_HOSTS` | Fully qualified domain names specified as a comma-separated list that are added to the ingress hosts. | +| `<ENVIRONMENT>_ADDITIONAL_HOSTS` | For a specific environment, the fully qualified domain names specified as a comma-separated list that are added to the ingress hosts. This takes precedence over `ADDITIONAL_HOSTS`. | | `POSTGRES_ENABLED` | Whether PostgreSQL is enabled; defaults to `"true"`. Set to `false` to disable the automatic deployment of PostgreSQL. | | `POSTGRES_USER` | The PostgreSQL user; defaults to `user`. Set it to use a custom username. | | `POSTGRES_PASSWORD` | The PostgreSQL password; defaults to `testing-password`. Set it to use a custom password. | diff --git a/doc/update/11.7-to-11.8.md b/doc/update/11.7-to-11.8.md new file mode 100644 index 00000000000..1587c310876 --- /dev/null +++ b/doc/update/11.7-to-11.8.md @@ -0,0 +1,393 @@ +--- +comments: false +--- + +# From 11.7 to 11.8 + +Make sure you view this update guide from the branch (version) of GitLab you would +like to install (e.g., `11-8-stable`. You can select the branch in the version +dropdown at the top left corner of GitLab (below the menu bar). + +If the highest number stable branch is unclear please check the +[GitLab Blog](https://about.gitlab.com/blog/archives.html) for installation +guide links by version. + +### 1. Stop server + +```bash +sudo service gitlab stop +``` + +### 2. Backup + +NOTE: If you installed GitLab from source, make sure `rsync` is installed. + +```bash +cd /home/git/gitlab + +sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production +``` + +### 3. Update Ruby + +NOTE: Beginning in GitLab 11.0, we only support Ruby 2.4 or higher, and dropped +support for Ruby 2.3. Be sure to upgrade if necessary. + +You can check which version you are running with `ruby -v`. + +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 + +./configure --disable-install-rdoc +make +sudo make install +``` + +Install Bundler: + +```bash +sudo gem install bundler --no-document +``` + +### 4. Update Node + +NOTE: Beginning in GitLab 11.8, we only support node 8 or higher, and dropped +support for node 6. Be sure to upgrade if necessary. + +GitLab utilizes [webpack](http://webpack.js.org) to compile frontend assets. +This requires a minimum version of node v8.10.0. + +You can check which version you are running with `node -v`. If you are running +a version older than `v8.10.0` you will need to update to a newer version. You +can find instructions to install from community maintained packages or compile +from source at the nodejs.org website. + +<https://nodejs.org/en/download/> + +GitLab also requires the use of yarn `>= v1.10.0` to manage JavaScript +dependencies. + +```bash +curl --silent --show-error https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add - +echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list +sudo apt-get update +sudo apt-get install yarn +``` + +More information can be found on the [yarn website](https://yarnpkg.com/en/docs/install). + +### 5. Update Go + +NOTE: GitLab 11.4 and higher only supports Go 1.10.x and newer, and dropped support for Go +1.9.x. Be sure to upgrade your installation if necessary. + +You can check which version you are running with `go version`. + +Download and install Go: + +```bash +# Remove former Go installation folder +sudo rm -rf /usr/local/go + +curl --remote-name --progress https://dl.google.com/go/go1.10.5.linux-amd64.tar.gz +echo 'a035d9beda8341b645d3f45a1b620cf2d8fb0c5eb409be36b389c0fd384ecc3a go1.10.5.linux-amd64.tar.gz' | shasum -a256 -c - && \ + sudo tar -C /usr/local -xzf go1.10.5.linux-amd64.tar.gz +sudo ln -sf /usr/local/go/bin/{go,godoc,gofmt} /usr/local/bin/ +rm go1.10.5.linux-amd64.tar.gz +``` + +### 6. Get latest code + +```bash +cd /home/git/gitlab + +sudo -u git -H git fetch --all --prune +sudo -u git -H git checkout -- db/schema.rb # local changes will be restored automatically +sudo -u git -H git checkout -- locale +``` + +For GitLab Community Edition: + +```bash +cd /home/git/gitlab + +sudo -u git -H git checkout 11-8-stable +``` + +OR + +For GitLab Enterprise Edition: + +```bash +cd /home/git/gitlab + +sudo -u git -H git checkout 11-8-stable-ee +``` + +### 7. Update gitlab-shell + +```bash +cd /home/git/gitlab-shell + +sudo -u git -H git fetch --all --tags --prune +sudo -u git -H git checkout v$(</home/git/gitlab/GITLAB_SHELL_VERSION) +sudo -u git -H bin/compile +``` + +### 8. Update gitlab-workhorse + +Install and compile gitlab-workhorse. GitLab-Workhorse uses +[GNU Make](https://www.gnu.org/software/make/). +If you are not using Linux you may have to run `gmake` instead of +`make` below. + +```bash +cd /home/git/gitlab-workhorse + +sudo -u git -H git fetch --all --tags --prune +sudo -u git -H git checkout v$(</home/git/gitlab/GITLAB_WORKHORSE_VERSION) +sudo -u git -H make +``` + +### 9. Update Gitaly + +#### Check Gitaly configuration + +Due to a bug in the `rake gitlab:gitaly:install` script your Gitaly +configuration file may contain syntax errors. The block name +`[[storages]]`, which may occur more than once in your `config.toml` +file, should be `[[storage]]` instead. + +```shell +sudo -u git -H sed -i.pre-10.1 's/\[\[storages\]\]/[[storage]]/' /home/git/gitaly/config.toml +``` + +#### Compile Gitaly + +```shell +cd /home/git/gitaly +sudo -u git -H git fetch --all --tags --prune +sudo -u git -H git checkout v$(</home/git/gitlab/GITALY_SERVER_VERSION) +sudo -u git -H make +``` + +### 10. Update gitlab-pages + +#### Only needed if you use GitLab Pages + +Install and compile gitlab-pages. GitLab-Pages uses +[GNU Make](https://www.gnu.org/software/make/). +If you are not using Linux you may have to run `gmake` instead of +`make` below. + +```bash +cd /home/git/gitlab-pages + +sudo -u git -H git fetch --all --tags --prune +sudo -u git -H git checkout v$(</home/git/gitlab/GITLAB_PAGES_VERSION) +sudo -u git -H make +``` + +### 11. Update MySQL permissions + +If you are using MySQL you need to grant the GitLab user the necessary +permissions on the database: + +```bash +mysql -u root -p -e "GRANT TRIGGER ON \`gitlabhq_production\`.* TO 'git'@'localhost';" +``` + +If you use MySQL with replication, or just have MySQL configured with binary logging, +you will need to also run the following on all of your MySQL servers: + +```bash +mysql -u root -p -e "SET GLOBAL log_bin_trust_function_creators = 1;" +``` + +You can make this setting permanent by adding it to your `my.cnf`: + +``` +log_bin_trust_function_creators=1 +``` + +### 12. Update configuration files + +#### New `unicorn.rb` configuration + +We have made [changes](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/22372) to `unicorn.rb` to allow GitLab run with both Unicorn and Puma in future. + +Make `/home/git/gitlab/config/unicorn.rb` the same as https://gitlab.com/gitlab-org/gitlab-ce/blob/11-8-stable/config/unicorn.rb.example but with your settings. +In particular, make sure that `require_relative "/home/git/gitlab/lib/gitlab/cluster/lifecycle_events"` line exists and the `before_exec`, `before_fork`, and `after_fork` handlers are configured as shown below: + +```ruby +require_relative "/home/git/gitlab/lib/gitlab/cluster/lifecycle_events" + +before_exec do |server| + # Signal application hooks that we're about to restart + Gitlab::Cluster::LifecycleEvents.do_master_restart +end + +before_fork do |server, worker| + # Signal application hooks that we're about to fork + Gitlab::Cluster::LifecycleEvents.do_before_fork +end + +after_fork do |server, worker| + # Signal application hooks of worker start + Gitlab::Cluster::LifecycleEvents.do_worker_start +end +``` + +#### New configuration options for `gitlab.yml` + +There might be configuration options available for [`gitlab.yml`][yaml]. View them with the command below and apply them manually to your current `gitlab.yml`: + +```sh +cd /home/git/gitlab + +git diff origin/11-7-stable:config/gitlab.yml.example origin/11-8-stable:config/gitlab.yml.example +``` + +#### Nginx configuration + +Ensure you're still up-to-date with the latest NGINX configuration changes: + +```sh +cd /home/git/gitlab + +# For HTTPS configurations +git diff origin/11-7-stable:lib/support/nginx/gitlab-ssl origin/11-8-stable:lib/support/nginx/gitlab-ssl + +# For HTTP configurations +git diff origin/11-7-stable:lib/support/nginx/gitlab origin/11-8-stable:lib/support/nginx/gitlab +``` + +If you are using Strict-Transport-Security in your installation to continue using it you must enable it in your Nginx +configuration as GitLab application no longer handles setting it. + +If you are using Apache instead of NGINX please see the updated [Apache templates]. +Also note that because Apache does not support upstreams behind Unix sockets you +will need to let gitlab-workhorse listen on a TCP port. You can do this +via [/etc/default/gitlab]. + +[Apache templates]: https://gitlab.com/gitlab-org/gitlab-recipes/tree/master/web-server/apache +[/etc/default/gitlab]: https://gitlab.com/gitlab-org/gitlab-ce/blob/11-8-stable/lib/support/init.d/gitlab.default.example#L38 + +#### SMTP configuration + +If you're installing from source and use SMTP to deliver mail, you will need to add the following line +to config/initializers/smtp_settings.rb: + +```ruby +ActionMailer::Base.delivery_method = :smtp +``` + +See [smtp_settings.rb.sample] as an example. + +[smtp_settings.rb.sample]: https://gitlab.com/gitlab-org/gitlab-ce/blob/11-8-stable/config/initializers/smtp_settings.rb.sample#L13 + +#### Init script + +There might be new configuration options available for [`gitlab.default.example`][gl-example]. View them with the command below and apply them manually to your current `/etc/default/gitlab`: + +```sh +cd /home/git/gitlab + +git diff origin/11-7-stable:lib/support/init.d/gitlab.default.example origin/11-8-stable:lib/support/init.d/gitlab.default.example +``` + +Ensure you're still up-to-date with the latest init script changes: + +```bash +cd /home/git/gitlab + +sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab +``` + +For Ubuntu 16.04.1 LTS: + +```bash +sudo systemctl daemon-reload +``` + +### 13. Install libs, migrations, etc. + +```bash +cd /home/git/gitlab + +# MySQL installations (note: the line below states '--without postgres') +sudo -u git -H bundle install --without postgres development test --deployment + +# PostgreSQL installations (note: the line below states '--without mysql') +sudo -u git -H bundle install --without mysql development test --deployment + +# Optional: clean up old gems +sudo -u git -H bundle clean + +# Run database migrations +sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production + +# Compile GetText PO files + +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 + +# Clean up cache +sudo -u git -H bundle exec rake cache:clear RAILS_ENV=production +``` + +**MySQL installations**: Run through the `MySQL strings limits` and `Tables and data conversion to utf8mb4` [tasks](../install/database_mysql.md). + +### 14. Start application + +```bash +sudo service gitlab start +sudo service nginx restart +``` + +### 15. Check application status + +Check if GitLab and its environment are configured correctly: + +```bash +cd /home/git/gitlab + +sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production +``` + +To make sure you didn't miss anything run a more thorough check: + +```bash +cd /home/git/gitlab + +sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production +``` + +If all items are green, then congratulations, the upgrade is complete! + +## Things went south? Revert to previous version (11.7) + +### 1. Revert the code to the previous version + +Follow the [upgrade guide from 11.6 to 11.7](11.6-to-11.7.md), except for the +database migration (the backup is already migrated to the previous version). + +### 2. Restore from the backup + +```bash +cd /home/git/gitlab + +sudo -u git -H bundle exec rake gitlab:backup:restore RAILS_ENV=production +``` + +If you have more than one backup `*.tar` file(s) please add `BACKUP=timestamp_of_backup` to the command above. + +[yaml]: https://gitlab.com/gitlab-org/gitlab-ce/blob/11-8-stable/config/gitlab.yml.example +[gl-example]: https://gitlab.com/gitlab-org/gitlab-ce/blob/11-8-stable/lib/support/init.d/gitlab.default.example diff --git a/doc/user/award_emojis.md b/doc/user/award_emojis.md index 93be3da44d4..e4fd08a582c 100644 --- a/doc/user/award_emojis.md +++ b/doc/user/award_emojis.md @@ -1,24 +1,24 @@ # Award emoji -> **Notes:** -> - First [introduced][1825] in GitLab 8.2. -> - GitLab 9.0 [introduced][ce-9570] the usage of native emojis if the platform +> - First [introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/1825) in GitLab 8.2. +> - GitLab 9.0 [introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9570) the usage of native emoji if the platform > supports them and falls back to images or CSS sprites. This change greatly -> improved the award emoji performance overall. +> improved award emoji performance overall. When you're collaborating online, you get fewer opportunities for high-fives -and thumbs-ups. Emoji can be awarded to issues, merge requests, snippets, and -virtually everywhere where you can have a discussion. +and thumbs-ups. Emoji can be awarded to [issues](project/issues/index.md), [merge requests](project/merge_requests/index.md), +[snippets](snippets.md), and anywhere you can have a discussion. ![Award emoji](img/award_emoji_select.png) Award emoji make it much easier to give and receive feedback without a long -comment thread. Comments that are only emoji will automatically become -award emoji. +comment thread. + +For information on the relevant API, see [Award Emoji API](../api/award_emoji.md). ## Sort issues and merge requests on vote count -> [Introduced][2871] in GitLab 8.5. +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/2781) in GitLab 8.5. You can quickly sort issues and merge requests by the number of votes they have received. The sort options can be found in the dropdown menu as "Most @@ -32,20 +32,16 @@ downvotes. ## Award emoji for comments -> [Introduced][4291] in GitLab 8.9. +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/4291) in GitLab 8.9. Award emoji can also be applied to individual comments when you want to celebrate an accomplishment or agree with an opinion. -To add an award emoji, click the smile in the top right of the comment and pick -an emoji from the dropdown. If you want to remove an award emoji, just click -the emoji again and the vote will be removed. +To: + +- Add an award emoji, click the smile in the top right of the comment and pick an emoji from the dropdown. +- Remove an award emoji, click the emoji again and the vote will be removed. ![Picking an emoji for a comment](img/award_emoji_comment_picker.png) ![An award emoji has been applied to a comment](img/award_emoji_comment_awarded.png) - -[2871]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/2781 -[1825]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/1825 -[4291]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/4291 -[ce-9570]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9570 diff --git a/doc/user/group/subgroups/index.md b/doc/user/group/subgroups/index.md index 4d56b33f684..b6f8f55978b 100644 --- a/doc/user/group/subgroups/index.md +++ b/doc/user/group/subgroups/index.md @@ -164,9 +164,11 @@ and you can choose the group of people to be notified. Here's a list of what you can't do with subgroups: -- [GitLab Pages](../../project/pages/index.md) are not currently working for - projects hosted under a subgroup. That means that only projects hosted under - the first parent group will work. +- [GitLab Pages](../../project/pages/index.md) supports projects hosted under + a subgroup, but not subgroup websites. + That means that only the highest-level group supports + [group websites](../../project/pages/introduction.html#user-or-group-pages), + although you can have project websites under a subgroup. - It is not possible to share a project with a group that's an ancestor of the group the project is in. That means you can only share as you walk down the hierarchy. For example, `group/subgroup01/project` **cannot** be shared diff --git a/doc/user/markdown.md b/doc/user/markdown.md index 893658290e5..f2448f240ca 100644 --- a/doc/user/markdown.md +++ b/doc/user/markdown.md @@ -300,7 +300,7 @@ You can use it to point out a <img src="https://gitlab.com/gitlab-org/gitlab-ce/ 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">. 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">. All you need to do is to look up one of the supported codes. -Consult the [Emoji Cheat Sheet](https://www.emojicopy.com) 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"> +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"> Most emoji are natively supported on macOS, Windows, iOS, Android and will fallback to image-based emoji where there is lack of support. diff --git a/doc/user/project/clusters/serverless/img/serverless-details.png b/doc/user/project/clusters/serverless/img/serverless-details.png Binary files differnew file mode 100644 index 00000000000..61e0735199a --- /dev/null +++ b/doc/user/project/clusters/serverless/img/serverless-details.png diff --git a/doc/user/project/clusters/serverless/img/serverless-page.png b/doc/user/project/clusters/serverless/img/serverless-page.png Binary files differindex 960d6e736d6..814b8532205 100644 --- a/doc/user/project/clusters/serverless/img/serverless-page.png +++ b/doc/user/project/clusters/serverless/img/serverless-page.png diff --git a/doc/user/project/clusters/serverless/index.md b/doc/user/project/clusters/serverless/index.md index ffce29f8f81..9ecb109fa89 100644 --- a/doc/user/project/clusters/serverless/index.md +++ b/doc/user/project/clusters/serverless/index.md @@ -103,7 +103,7 @@ In order to deploy functions to your Knative instance, the following files must The `gitlab-ci.yml` template creates a `Deploy` stage with a `functions` job that invokes the `tm` CLI with the required parameters. 2. `serverless.yml`: This file contains the metadata for your functions, - such as name, runtime, and environment. It must be included at the root of your repository. The following is a sample `echo` function which shows the required structure for the file. + such as name, runtime, and environment. It must be included at the root of your repository. The following is a sample `echo` function which shows the required structure for the file. You can find the relevant files for this project in the [functions example project](https://gitlab.com/knative-examples/functions). ```yaml service: my-functions @@ -127,7 +127,7 @@ In order to deploy functions to your Knative instance, the following files must ``` -The `serverless.yml` file contains three sections with distinct parameters: +The `serverless.yml` file is referencing both an `echo` directory (under `buildargs`) and an `echo` file (under `handler`) which is a reference to `echo.js` in the [repository](https://gitlab.com/knative-examples/functions). Additionally, it contains three sections with distinct parameters: ### `service` @@ -167,8 +167,8 @@ appear under **Operations > Serverless**. ![serverless page](img/serverless-page.png) -This page contains all functions available for the project, the URL for -accessing the function, and if available, the function's runtime information. +This page contains all functions available for the project, the description for +accessing the function, and, if available, the function's runtime information. The details are derived from the Knative installation inside each of the project's Kubernetes cluster. @@ -184,6 +184,12 @@ The sample function can now be triggered from any HTTP client using a simple `PO Currently, the Serverless page presents all functions available in all clusters registered for the project with Knative installed. +Clicking on the function name will provide additional details such as the +function's URL as well as runtime statistics such as the number of active pods +available to service the request based on load. + +![serverless function details](img/serverless-details.png) + ## Deploying Serverless applications > Introduced in GitLab 11.5. diff --git a/doc/user/project/img/issue_boards_multiple.png b/doc/user/project/img/issue_boards_multiple.png Binary files differindex 7bb088aad0b..242460925f7 100644 --- a/doc/user/project/img/issue_boards_multiple.png +++ b/doc/user/project/img/issue_boards_multiple.png diff --git a/doc/user/project/index.md b/doc/user/project/index.md index d46ae31580a..ce8bd2de61f 100644 --- a/doc/user/project/index.md +++ b/doc/user/project/index.md @@ -65,8 +65,8 @@ common actions on issues or merge requests browse, and download job artifacts - [Pipeline settings](pipelines/settings.md): Set up Git strategy (choose the default way your repository is fetched from GitLab in a job), timeout (defines the maximum amount of time in minutes that a job is able run), custom path for `.gitlab-ci.yml`, test coverage parsing, pipeline's visibility, and much more - - [GKE cluster integration](clusters/index.md): Connecting your GitLab project - with Google Kubernetes Engine + - [Kubernetes cluster integration](clusters/index.md): Connecting your GitLab project + with a Kubernetes cluster - [GitLab Pages](pages/index.md): Build, test, and deploy your static website with GitLab Pages diff --git a/doc/user/project/issue_board.md b/doc/user/project/issue_board.md index 9e2434c02ec..7962eeada5c 100644 --- a/doc/user/project/issue_board.md +++ b/doc/user/project/issue_board.md @@ -175,6 +175,7 @@ products. Clicking on the current board name in the upper left corner will reveal a menu from where you can create another Issue Board and rename or delete the existing one. +Using the search box at the top of the menu, you can filter the listed boards. When you're revisiting an issue board in a project or group with multiple boards, GitLab will automatically load the last board you visited. diff --git a/doc/user/project/members/index.md b/doc/user/project/members/index.md index 2c2e8e2d556..2a490e3fd7f 100644 --- a/doc/user/project/members/index.md +++ b/doc/user/project/members/index.md @@ -77,7 +77,7 @@ GitLab users to the project. Once done, hit **Add users to project** and watch that there is a new member with the e-mail address we used above. From there on, you can resend the -invitation, change their access level or even delete them. +invitation, change their access level, or even delete them. ![Invite user members list](img/add_user_email_accept.png) diff --git a/doc/user/project/merge_requests/allow_collaboration.md b/doc/user/project/merge_requests/allow_collaboration.md index 859ac92ef89..da6e6b5fd3a 100644 --- a/doc/user/project/merge_requests/allow_collaboration.md +++ b/doc/user/project/merge_requests/allow_collaboration.md @@ -1,20 +1,72 @@ # Allow collaboration on merge requests across forks -> [Introduced][ce-17395] in GitLab 10.6. +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/17395) + in GitLab 10.6. + +When a user opens a merge request from a fork, they are given the option to allow +upstream members to collaborate with them on the source branch. This allows +the members of the upstream project to make small fixes or rebase branches +before merging, reducing the back and forth of accepting external contributions. This feature is available for merge requests across forked projects that are -publicly accessible. It makes it easier for members of projects to -collaborate on merge requests across forks. +publicly accessible. When enabled for a merge request, members with merge access to the target branch of the project will be granted write permissions to the source branch of the merge request. +## Enabling commit edits from upstream members + The feature can only be enabled by users who already have push access to the -source project, and only lasts while the merge request is open. +source project and only lasts while the merge request is open. Once enabled, +upstream members will also be able to retry the pipelines and jobs of the +merge request: + +1. Enable the contribution while creating or editing a merge request. + + ![Enable contribution](img/allow_collaboration.png) + +1. Once the merge request is created, you'll see that commits from members who + can merge to the target branch are allowed. + + ![Check that contribution is enabled](img/allow_collaboration_after_save.png) + +## Pushing to the fork as the upstream member + +If the creator of the merge request has enabled contributions from upstream +members, you can push directly to the branch of the forked repository. + +Assuming that: + +- The forked project URL is `git@gitlab.com:thedude/awesome-project.git`. +- The branch of the merge request is `update-docs`. + +Here's how the process would look like: + +1. First, you need to get the changes that the merge request has introduced. + Click the **Check out branch** button that has some pre-populated + commands that you can run. + + ![Check out branch button](img/checkout_button.png) + +1. Use the copy to clipboard button to copy the first command and paste them + in your terminal: + + ```sh + git fetch git@gitlab.com:thedude/awesome-project.git update-docs + git checkout -b thedude-awesome-project-update-docs FETCH_HEAD + ``` + + This will fetch the branch of the forked project and then create a local branch + based off the fetched branch. -Enable this functionality while creating or editing a merge request: +1. Make any changes you want and commit. +1. Push to the forked project: -![Enable collaboration](./img/allow_collaboration.png) + ```sh + git push git@gitlab.com:thedude/awesome-project.git thedude-awesome-project-update-docs:update-docs + ``` -[ce-17395]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/17395 + Note the colon (`:`) between the two branches. The above command will push the + local branch `thedude-awesome-project-update-docs` to the + `update-docs` branch of the `git@gitlab.com:thedude/awesome-project.git` repository. diff --git a/doc/user/project/merge_requests/img/allow_collaboration.png b/doc/user/project/merge_requests/img/allow_collaboration.png Binary files differindex 3c81e4c27b8..e40e8a6b11c 100644 --- a/doc/user/project/merge_requests/img/allow_collaboration.png +++ b/doc/user/project/merge_requests/img/allow_collaboration.png diff --git a/doc/user/project/merge_requests/img/allow_collaboration_after_save.png b/doc/user/project/merge_requests/img/allow_collaboration_after_save.png Binary files differnew file mode 100644 index 00000000000..4ba4c84c8c5 --- /dev/null +++ b/doc/user/project/merge_requests/img/allow_collaboration_after_save.png diff --git a/doc/user/project/merge_requests/img/checkout_button.png b/doc/user/project/merge_requests/img/checkout_button.png Binary files differnew file mode 100644 index 00000000000..9850795c9b4 --- /dev/null +++ b/doc/user/project/merge_requests/img/checkout_button.png diff --git a/doc/user/project/merge_requests/index.md b/doc/user/project/merge_requests/index.md index f479f9e4ef6..9015a964781 100644 --- a/doc/user/project/merge_requests/index.md +++ b/doc/user/project/merge_requests/index.md @@ -92,6 +92,15 @@ request widget will show the "Removes source branch" text. ![Remove source branch status](img/remove_source_branch_status.png) +## Allow collaboration on merge requests across forks + +When a user opens a merge request from a fork, they are given the option to allow +upstream maintainers to collaborate with them on the source branch. This allows +the maintainers of the upstream project to make small fixes or rebase branches +before merging, reducing the back and forth of accepting community contributions. + +[Learn more about allowing upstream members to push to forks.](allow_collaboration.md) + ## Authorization for merge requests There are two main ways to have a merge request flow with GitLab: diff --git a/doc/user/project/operations/error_tracking.md b/doc/user/project/operations/error_tracking.md new file mode 100644 index 00000000000..2b5abc7233f --- /dev/null +++ b/doc/user/project/operations/error_tracking.md @@ -0,0 +1,30 @@ +# Error Tracking + +> [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/169) in GitLab 11.7. + +Error tracking allows developers to easily discover and view the errors that their application may be generating. By surfacing error information where the code is being developed, efficiency and awareness can be increased. + +## Sentry error tracking + +[Sentry](https://sentry.io/) is an open source error tracking system. GitLab allows administrators to connect Sentry to GitLab, to allow users to view a list of Sentry errors in GitLab itself. + +### Deploying Sentry + +You may sign up to the cloud hosted https://sentry.io or deploy your own [on-premise instance](https://docs.sentry.io/server/installation/). + +### Enabling Sentry + +GitLab provides an easy way to connect Sentry to your project: + +1. Sign up to Sentry.io or [deploy your own](#deploying-sentry) Sentry instance. +1. [Find or generate](https://docs.sentry.io/api/auth/) a Sentry auth token for your Sentry project. +1. Navigate to your project’s **Settings > Operations** and provide the Sentry API URL and auth token. +1. Ensure that the 'Active' checkbox is set. +1. Click **Save changes** for the changes to take effect. +1. You can now visit **Operations > Error Tracking** in your project's sidebar to [view a list](#error-tracking-list) of Sentry errors. + +## Error Tracking List + +The Error Tracking list may be found at **Operations > Error Tracking** in your project's sidebar. + +![Error Tracking list](img/error_tracking_list.png) diff --git a/doc/user/project/operations/img/error_tracking_list.png b/doc/user/project/operations/img/error_tracking_list.png Binary files differnew file mode 100644 index 00000000000..aa0f9867fdb --- /dev/null +++ b/doc/user/project/operations/img/error_tracking_list.png diff --git a/doc/user/project/pages/getting_started_part_one.md b/doc/user/project/pages/getting_started_part_one.md index 290dfa5af84..595241b2cba 100644 --- a/doc/user/project/pages/getting_started_part_one.md +++ b/doc/user/project/pages/getting_started_part_one.md @@ -85,6 +85,12 @@ and a project within this group is called `blog`. Your project URL is `https://gitlab.com/websites/blog/`. Once you enable GitLab Pages for this project, the site will live under `https://websites.gitlab.io/blog/`. +- You created a group for your engineering department called `engineering`, +a subgroup for all your documentation websites called `docs`, +and a project within this subgroup is called `workflows`. Your project +URL is `https://gitlab.com/engineering/docs/workflows/`. Once you enable +GitLab Pages for this project, the site will live under +`https://engineering.gitlab.io/docs/workflows`. #### User and Group Websites @@ -97,9 +103,7 @@ will be published under `https://john.gitlab.io`. Once you enable GitLab Pages for your project, your website will be published under `https://websites.gitlab.io`. ->**Note:** -GitLab Pages [does **not** support subgroups](../../group/subgroups/index.md#limitations). -You can only create the highest level group website. +> Support for subgroup project's websites was [introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/30548) in GitLab 11.8. **General example:** diff --git a/doc/user/project/pages/introduction.md b/doc/user/project/pages/introduction.md index ed049e2e648..a7846b1ee18 100644 --- a/doc/user/project/pages/introduction.md +++ b/doc/user/project/pages/introduction.md @@ -38,6 +38,7 @@ be served on. | Group pages | `groupname.example.io` | `http(s)://groupname.example.io` | | Project pages owned by a user | `projectname` | `http(s)://username.example.io/projectname` | | Project pages owned by a group | `projectname` | `http(s)://groupname.example.io/projectname`| +| Project pages owned by a subgroup | `subgroup/projectname` | `http(s)://groupname.example.io/subgroup/projectname`| > **Warning:** > There are some known [limitations](#limitations) regarding namespaces served @@ -494,8 +495,8 @@ don't redirect HTTP to HTTPS. [rfc]: https://tools.ietf.org/html/rfc2818#section-3.1 "HTTP Over TLS RFC" -GitLab Pages [does **not** support subgroups](../../group/subgroups/index.md#limitations). -You can only create the highest level group website. +GitLab Pages [does **not** support group websites for subgroups](../../group/subgroups/index.md#limitations). +You can only create the highest-level group website. ## Redirects in GitLab Pages diff --git a/doc/user/project/pipelines/schedules.md b/doc/user/project/pipelines/schedules.md index 051277dfe02..ec8b8444d99 100644 --- a/doc/user/project/pipelines/schedules.md +++ b/doc/user/project/pipelines/schedules.md @@ -3,7 +3,7 @@ > **Notes**: > - This feature was introduced in 9.1 as [Trigger Schedule][ce-10533]. > - In 9.2, the feature was [renamed to Pipeline Schedule][ce-10853]. -> - Cron notation is parsed by [Rufus-Scheduler](https://github.com/jmettraux/rufus-scheduler). +> - Cron notation is parsed by [Fugit](https://github.com/floraison/fugit). Pipeline schedules can be used to run a pipeline at specific intervals, for example every month on the 22nd for a certain branch. diff --git a/doc/user/project/releases/index.md b/doc/user/project/releases/index.md index 890d6fbc6c7..00a4f6c6a6b 100644 --- a/doc/user/project/releases/index.md +++ b/doc/user/project/releases/index.md @@ -12,7 +12,7 @@ GitLab's **Releases** are a way to track deliverables in your project. Consider a snapshot in time of the source, build output, and other metadata or artifacts associated with a released version of your code. -At the moment, you can create Release entries via the [Releases API](../../../api/releases.md); +At the moment, you can create Release entries via the [Releases API](../../../api/releases/index.md); we recommend doing this as one of the last steps in your CI/CD release pipeline. ## Getting started with Releases @@ -51,6 +51,9 @@ A link is any URL which can point to whatever you like; documentation, built binaries, or other related materials. These can be both internal or external links from your GitLab instance. +NOTE: **NOTE** +You can manipulate links of each release entry with [Release Links API](../../../api/releases/links.md) + ## Releases list Navigate to **Project > Releases** in order to see the list of releases for a given diff --git a/doc/user/project/repository/branches/index.md b/doc/user/project/repository/branches/index.md index 783081cec26..f05554ffc5b 100644 --- a/doc/user/project/repository/branches/index.md +++ b/doc/user/project/repository/branches/index.md @@ -2,16 +2,17 @@ Read through GiLab's branching documentation: -- [Create a branch](../web_editor.md#create-a-new-branch) -- [Default branch](#default-branch) -- [Protected branches](../../protected_branches.md#protected-branches) -- [Delete merged branches](#delete-merged-branches) -- [Branch filter search box](#branch-filter-search-box) +- [Create a branch](../web_editor.md#create-a-new-branch). +- [Default branch](#default-branch). +- [Protected branches](../../protected_branches.md#protected-branches). +- [Delete merged branches](#delete-merged-branches). +- [Branch filter search box](#branch-filter-search-box). See also: -- [GitLab Flow](../../../../university/training/gitlab_flow.md#gitlab-flow): use the best of GitLab for your branching strategies -- [Getting started with Git](../../../../topics/git/index.md) and GitLab +- [Branches API](../../../../api/branches.md), for information on operating on repository branches using the GitLab API. +- [GitLab Flow](../../../../university/training/gitlab_flow.md#gitlab-flow). Use the best of GitLab for your branching strategies. +- [Getting started with Git](../../../../topics/git/index.md) and GitLab. ## Default branch @@ -41,7 +42,6 @@ this operation. It's particularly useful to clean up old branches that were not deleted automatically when a merge request was merged. - ## Branch filter search box > [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/22166) in GitLab 11.5. diff --git a/doc/user/project/repository/gpg_signed_commits/index.md b/doc/user/project/repository/gpg_signed_commits/index.md index c6239c8e41c..c7e20f01a75 100644 --- a/doc/user/project/repository/gpg_signed_commits/index.md +++ b/doc/user/project/repository/gpg_signed_commits/index.md @@ -4,7 +4,7 @@ NOTE: **Note:** The term GPG is used for all OpenPGP/PGP/GPG related material and implementations. -> - [Introduced][ce-9546] in GitLab 9.5. +> - [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9546) in GitLab 9.5. > - Subkeys support was added in GitLab 10.1. GitLab can show whether a commit is verified or not when signed with a GPG key. @@ -36,30 +36,22 @@ to be met: ## Generating a GPG key -> **Notes:** -> - If your Operating System has `gpg2` installed, replace `gpg` with `gpg2` in -> the following commands. -> - If Git is using `gpg` and you get errors like `secret key not available` or -> `gpg: signing failed: secret key not available`, run the following command to -> change to `gpg2`: -> -> ``` -> git config --global gpg.program gpg2 -> ``` - If you don't already have a GPG key, the following steps will help you get started: -1. [Install GPG](https://www.gnupg.org/download/index.html) for your operating system -1. Generate the private/public key pair with the following command: +1. [Install GPG](https://www.gnupg.org/download/index.html) for your operating system. + If your Operating System has `gpg2` installed, replace `gpg` with `gpg2` in + the following commands. +1. Generate the private/public key pair with the following command, which will + spawn a series of questions: ```sh gpg --full-gen-key ``` - - _NOTE: In some cases like Gpg4win on Windows and other Mac OS versions the command here may be ` gpg --gen-key`_ - This will spawn a series of questions. + NOTE: **Note:** + In some cases like Gpg4win on Windows and other macOS versions, the command + here may be `gpg --gen-key`. 1. The first question is which algorithm can be used. Select the kind you want or press <kbd>Enter</kbd> to choose the default (RSA and RSA): @@ -109,10 +101,10 @@ started: GnuPG needs to construct a user ID to identify your key. Real name: Mr. Robot - Email address: mr@robot.sh + Email address: <your_email> Comment: You selected this USER-ID: - "Mr. Robot <mr@robot.sh>" + "Mr. Robot <your_email>" Change (N)ame, (C)omment, (E)mail or (O)kay/(Q)uit? O ``` @@ -121,10 +113,10 @@ started: 1. Use the following command to list the private GPG key you just created: ``` - gpg --list-secret-keys --keyid-format LONG mr@robot.sh + gpg --list-secret-keys --keyid-format LONG <your_email> ``` - Replace `mr@robot.sh` with the email address you entered above. + Replace `<your_email>` with the email address you entered above. 1. Copy the GPG key ID that starts with `sec`. In the following example, that's `30F2B65B9246B6CA`: @@ -132,7 +124,7 @@ started: ``` sec rsa4096/30F2B65B9246B6CA 2017-08-18 [SC] D5E4F29F3275DC0CDA8FFC8730F2B65B9246B6CA - uid [ultimate] Mr. Robot <mr@robot.sh> + uid [ultimate] Mr. Robot <your_email> ssb rsa4096/B7ABC0813E4028C0 2017-08-18 [E] ``` @@ -146,7 +138,7 @@ started: ## Adding a GPG key to your account ->**Note:** +NOTE: **Note:** Once you add a key, you cannot edit it, only remove it. In case the paste didn't work, you'll have to remove the offending key and re-add it. @@ -174,11 +166,11 @@ key to use. 1. Use the following command to list the private GPG key you just created: - ``` - gpg --list-secret-keys --keyid-format LONG mr@robot.sh + ```sh + gpg --list-secret-keys --keyid-format LONG <your_email> ``` - Replace `mr@robot.sh` with the email address you entered above. + Replace `<your_email>` with the email address you entered above. 1. Copy the GPG key ID that starts with `sec`. In the following example, that's `30F2B65B9246B6CA`: @@ -186,18 +178,27 @@ key to use. ``` sec rsa4096/30F2B65B9246B6CA 2017-08-18 [SC] D5E4F29F3275DC0CDA8FFC8730F2B65B9246B6CA - uid [ultimate] Mr. Robot <mr@robot.sh> + uid [ultimate] Mr. Robot <your_email> ssb rsa4096/B7ABC0813E4028C0 2017-08-18 [E] ``` 1. Tell Git to use that key to sign the commits: - ``` + ```sh git config --global user.signingkey 30F2B65B9246B6CA ``` Replace `30F2B65B9246B6CA` with your GPG key ID. + +1. (Optional) If Git is using `gpg` and you get errors like `secret key not available` + or `gpg: signing failed: secret key not available`, run the following command to + change to `gpg2`: + + ```sh + git config --global gpg.program gpg2 + ``` + ## Signing commits After you have [created your GPG key](#generating-a-gpg-key) and [added it to @@ -261,4 +262,7 @@ To remove a GPG key from your account: 1. Navigate to the **GPG keys** tab. 1. Click on the trash icon besides the GPG key you want to delete. -[ce-9546]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9546 +## Rejecting commits that are not signed **[PREMIUM]** + +You can configure your project to reject commits that aren't GPG-signed +via [push rules](https://docs.gitlab.com/ee/push_rules/push_rules.html). diff --git a/doc/user/project/settings/img/general_settings.png b/doc/user/project/settings/img/general_settings.png Binary files differindex 96f5b84871f..4ff6fff5ca3 100644 --- a/doc/user/project/settings/img/general_settings.png +++ b/doc/user/project/settings/img/general_settings.png diff --git a/doc/user/project/settings/import_export.md b/doc/user/project/settings/import_export.md index 3bbfa74f4b7..89008fd15b9 100644 --- a/doc/user/project/settings/import_export.md +++ b/doc/user/project/settings/import_export.md @@ -61,7 +61,7 @@ The following items will be exported: - Project and wiki repositories - Project uploads -- Project configuration including web hooks and services +- Project configuration, including services - Issues with comments, merge requests with diffs and comments, labels, milestones, snippets, and other project entities - LFS objects diff --git a/doc/user/project/settings/index.md b/doc/user/project/settings/index.md index d6754372816..b09a3f927d1 100644 --- a/doc/user/project/settings/index.md +++ b/doc/user/project/settings/index.md @@ -14,7 +14,7 @@ functionality of a project. ### General project settings -Adjust your project's name, description, avatar, [default branch](../repository/branches/index.md#default-branch), and tags: +Adjust your project's name, description, avatar, [default branch](../repository/branches/index.md#default-branch), and topics: ![general project settings](img/general_settings.png) @@ -122,3 +122,9 @@ GitLab administrators can use the admin interface to move any project to any namespace if needed. [permissions]: ../../permissions.md##project-members-permissions + +## Operations settings + +### Error Tracking + +Configure Error Tracking to discover and view [Sentry errors within GitLab](../operations/error_tracking.md). diff --git a/lib/api/api.rb b/lib/api/api.rb index 59b67c67f9d..a768b78cda5 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -110,6 +110,7 @@ module API mount ::API::GroupMilestones mount ::API::Groups mount ::API::GroupVariables + mount ::API::ImportGithub mount ::API::Internal mount ::API::Issues mount ::API::JobArtifacts diff --git a/lib/api/entities.rb b/lib/api/entities.rb index a2a3c0a16d7..57a48d0c0fa 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -115,6 +115,9 @@ module API expose :group_name do |group_link, options| group_link.group.name end + expose :group_full_path do |group_link, options| + group_link.group.full_path + end expose :group_access, as: :group_access_level expose :expires_at end @@ -277,7 +280,7 @@ module API # N+1 is solved then by using `subject.tags.map(&:name)` # MR describing the solution: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/20555 super(projects_relation).preload(:group) - .preload(project_group_links: :group, + .preload(project_group_links: { group: :route }, fork_network: :root_project, fork_network_member: :forked_from_project, forked_from_project: [:route, :forks, :tags, namespace: :route]) diff --git a/lib/api/features.rb b/lib/api/features.rb index 1331248699f..835aac05905 100644 --- a/lib/api/features.rb +++ b/lib/api/features.rb @@ -16,15 +16,13 @@ module API end end - # rubocop: disable CodeReuse/ActiveRecord def gate_targets(params) - targets = [] - targets << Feature.group(params[:feature_group]) if params[:feature_group] - targets << UserFinder.new(params[:user]).find_by_username if params[:user] + Feature::Target.new(params).targets + end - targets + def gate_specified?(params) + Feature::Target.new(params).gate_specified? end - # rubocop: enable CodeReuse/ActiveRecord end resource :features do @@ -44,6 +42,7 @@ module API requires :value, type: String, desc: '`true` or `false` to enable/disable, an integer for percentage of time' optional :feature_group, type: String, desc: 'A Feature group name' optional :user, type: String, desc: 'A GitLab username' + optional :project, type: String, desc: 'A projects path, like gitlab-org/gitlab-ce' end post ':name' do feature = Feature.get(params[:name]) @@ -52,13 +51,13 @@ module API case value when true - if targets.present? + if gate_specified?(params) targets.each { |target| feature.enable(target) } else feature.enable end when false - if targets.present? + if gate_specified?(params) targets.each { |target| feature.disable(target) } else feature.disable diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index 6c1a730935a..fa6c9777824 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -235,8 +235,8 @@ module API forbidden! unless current_user.admin? end - def authorize!(action, subject = :global) - forbidden! unless can?(current_user, action, subject) + def authorize!(action, subject = :global, reason = nil) + forbidden!(reason) unless can?(current_user, action, subject) end def authorize_push_project @@ -496,7 +496,11 @@ module API def send_git_blob(repository, blob) env['api.format'] = :txt content_type 'text/plain' - header['Content-Disposition'] = content_disposition('attachment', blob.name) + header['Content-Disposition'] = content_disposition('inline', blob.name) + + # Let Workhorse examine the content and determine the better content disposition + header[Gitlab::Workhorse::DETECT_HEADER] = "true" + header(*Gitlab::Workhorse.send_git_blob(repository, blob)) end @@ -512,7 +516,7 @@ module API # `request`. We workaround this by defining methods that returns the right # values. def define_params_for_grape_middleware - self.define_singleton_method(:request) { Rack::Request.new(env) } + self.define_singleton_method(:request) { ActionDispatch::Request.new(env) } self.define_singleton_method(:params) { request.params.symbolize_keys } end diff --git a/lib/api/import_github.rb b/lib/api/import_github.rb new file mode 100644 index 00000000000..bb4e536cf57 --- /dev/null +++ b/lib/api/import_github.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +module API + class ImportGithub < Grape::API + rescue_from Octokit::Unauthorized, with: :provider_unauthorized + + helpers do + def client + @client ||= Gitlab::LegacyGithubImport::Client.new(params[:personal_access_token], client_options) + end + + def access_params + { github_access_token: params[:personal_access_token] } + end + + def client_options + {} + end + + def provider + :github + end + end + + desc 'Import a GitHub project' do + detail 'This feature was introduced in GitLab 11.3.4.' + success Entities::ProjectEntity + end + params do + requires :personal_access_token, type: String, desc: 'GitHub personal access token' + requires :repo_id, type: Integer, desc: 'GitHub repository ID' + optional :new_name, type: String, desc: 'New repo name' + requires :target_namespace, type: String, desc: 'Namespace to import repo into' + end + post 'import/github' do + result = Import::GithubService.new(client, current_user, params).execute(access_params, provider) + + if result[:status] == :success + present ProjectSerializer.new.represent(result[:project]) + else + status result[:http_status] + { errors: result[:message] } + end + end + end +end diff --git a/lib/api/job_artifacts.rb b/lib/api/job_artifacts.rb index a4068a200b3..933bd067e26 100644 --- a/lib/api/job_artifacts.rb +++ b/lib/api/job_artifacts.rb @@ -23,17 +23,14 @@ module API requires :job, type: String, desc: 'The name for the job' end route_setting :authentication, job_token_allowed: true - # rubocop: disable CodeReuse/ActiveRecord get ':id/jobs/artifacts/:ref_name/download', requirements: { ref_name: /.+/ } do authorize_download_artifacts! - builds = user_project.latest_successful_builds_for(params[:ref_name]) - latest_build = builds.find_by!(name: params[:job]) + latest_build = user_project.latest_successful_build_for!(params[:job], params[:ref_name]) present_carrierwave_file!(latest_build.artifacts_file) end - # rubocop: enable CodeReuse/ActiveRecord desc 'Download a specific file from artifacts archive from a ref' do detail 'This feature was introduced in GitLab 11.5' @@ -48,7 +45,7 @@ module API requirements: { ref_name: /.+/ } do authorize_download_artifacts! - build = user_project.latest_successful_build_for(params[:job], params[:ref_name]) + build = user_project.latest_successful_build_for!(params[:job], params[:ref_name]) path = Gitlab::Ci::Build::Artifacts::Path .new(params[:artifact_path]) diff --git a/lib/api/project_clusters.rb b/lib/api/project_clusters.rb index 7aada260297..c96261a7b57 100644 --- a/lib/api/project_clusters.rb +++ b/lib/api/project_clusters.rb @@ -63,7 +63,7 @@ module API use :create_params_ee end post ':id/clusters/user' do - authorize! :create_cluster, user_project + authorize! :add_cluster, user_project, 'Instance does not support multiple Kubernetes clusters' user_cluster = ::Clusters::CreateService .new(current_user, create_cluster_user_params) diff --git a/lib/api/release/links.rb b/lib/api/release/links.rb index a75a320e929..e3072684ef7 100644 --- a/lib/api/release/links.rb +++ b/lib/api/release/links.rb @@ -8,8 +8,6 @@ module API RELEASE_ENDPOINT_REQUIREMETS = API::NAMESPACE_OR_PROJECT_REQUIREMENTS .merge(tag_name: API::NO_SLASH_URL_PART_REGEX) - before { error!('404 Not Found', 404) unless Feature.enabled?(:releases_page, user_project) } - params do requires :id, type: String, desc: 'The ID of a project' end diff --git a/lib/api/releases.rb b/lib/api/releases.rb index c3d4101528c..576fee51db0 100644 --- a/lib/api/releases.rb +++ b/lib/api/releases.rb @@ -7,7 +7,6 @@ module API RELEASE_ENDPOINT_REQUIREMETS = API::NAMESPACE_OR_PROJECT_REQUIREMENTS .merge(tag_name: API::NO_SLASH_URL_PART_REGEX) - before { error!('404 Not Found', 404) unless Feature.enabled?(:releases_page, user_project) } before { authorize_read_releases! } params do diff --git a/lib/api/settings.rb b/lib/api/settings.rb index f53ba0ab761..95371961398 100644 --- a/lib/api/settings.rb +++ b/lib/api/settings.rb @@ -35,7 +35,7 @@ module API end optional :container_registry_token_expire_delay, type: Integer, desc: 'Authorization token duration (minutes)' optional :default_artifacts_expire_in, type: String, desc: "Set the default expiration time for each job's artifacts" - optional :default_branch_protection, type: Integer, values: [0, 1, 2], desc: 'Determine if developers can push to master' + optional :default_branch_protection, type: Integer, values: Gitlab::Access.protection_values, desc: 'Determine if developers can push to master' optional :default_group_visibility, type: String, values: Gitlab::VisibilityLevel.string_values, desc: 'The default group visibility' optional :default_project_visibility, type: String, values: Gitlab::VisibilityLevel.string_values, desc: 'The default project visibility' optional :default_projects_limit, type: Integer, desc: 'The maximum number of personal projects' diff --git a/lib/api/wikis.rb b/lib/api/wikis.rb index 302b2797a34..ef0e3decc2c 100644 --- a/lib/api/wikis.rb +++ b/lib/api/wikis.rb @@ -22,7 +22,9 @@ module API end end - resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do + WIKI_ENDPOINT_REQUIREMENTS = API::NAMESPACE_OR_PROJECT_REQUIREMENTS.merge(slug: API::NO_SLASH_URL_PART_REGEX) + + resource :projects, requirements: WIKI_ENDPOINT_REQUIREMENTS do desc 'Get a list of wiki pages' do success Entities::WikiPageBasic end @@ -103,7 +105,7 @@ module API requires :file, type: ::API::Validations::Types::SafeFile, desc: 'The attachment file to be uploaded' optional :branch, type: String, desc: 'The name of the branch' end - post ":id/wikis/attachments", requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do + post ":id/wikis/attachments" do authorize! :create_wiki, user_project result = ::Wikis::CreateAttachmentService.new(user_project, diff --git a/lib/backup/files.rb b/lib/backup/files.rb index 0032ae8f84b..2bac84846c5 100644 --- a/lib/backup/files.rb +++ b/lib/backup/files.rb @@ -71,8 +71,14 @@ module Backup end def run_pipeline!(cmd_list, options = {}) - status_list = Open3.pipeline(*cmd_list, options) - raise Backup::Error, 'Backup failed' unless status_list.compact.all?(&:success?) + err_r, err_w = IO.pipe + options[:err] = err_w + status = Open3.pipeline(*cmd_list, options) + err_w.close + return if status.compact.all?(&:success?) + + regex = /^g?tar: \.: Cannot mkdir: No such file or directory$/ + raise Backup::Error, 'Backup failed' unless err_r.read =~ regex end end end diff --git a/lib/backup/manager.rb b/lib/backup/manager.rb index 0add2b3f875..06b0338b1ed 100644 --- a/lib/backup/manager.rb +++ b/lib/backup/manager.rb @@ -50,6 +50,7 @@ module Backup if directory.files.create(key: remote_target, body: File.open(tar_file), public: false, multipart_chunk_size: Gitlab.config.backup.upload.multipart_chunk_size, encryption: Gitlab.config.backup.upload.encryption, + encryption_key: Gitlab.config.backup.upload.encryption_key, storage_class: Gitlab.config.backup.upload.storage_class) progress.puts "done".color(:green) else diff --git a/lib/banzai/filter/milestone_reference_filter.rb b/lib/banzai/filter/milestone_reference_filter.rb index c70c3f0c04e..fce042e8946 100644 --- a/lib/banzai/filter/milestone_reference_filter.rb +++ b/lib/banzai/filter/milestone_reference_filter.rb @@ -101,9 +101,9 @@ module Banzai def self_and_ancestors_ids(parent) if group_context?(parent) - parent.self_and_ancestors_ids + parent.self_and_ancestors.select(:id) elsif project_context?(parent) - parent.group&.self_and_ancestors_ids + parent.group&.self_and_ancestors&.select(:id) end end diff --git a/lib/bitbucket_server/client.rb b/lib/bitbucket_server/client.rb index 83e8808db07..6a608058813 100644 --- a/lib/bitbucket_server/client.rb +++ b/lib/bitbucket_server/client.rb @@ -4,18 +4,6 @@ module BitbucketServer class Client attr_reader :connection - ServerError = Class.new(StandardError) - - SERVER_ERRORS = [SocketError, - OpenSSL::SSL::SSLError, - Errno::ECONNRESET, - Errno::ECONNREFUSED, - Errno::EHOSTUNREACH, - Net::OpenTimeout, - Net::ReadTimeout, - Gitlab::HTTP::BlockedUrlError, - BitbucketServer::Connection::ConnectionError].freeze - def initialize(options = {}) @connection = Connection.new(options) end @@ -64,8 +52,6 @@ module BitbucketServer def get_collection(path, type, page_offset: 0, limit: nil) paginator = BitbucketServer::Paginator.new(connection, Addressable::URI.escape(path), type, page_offset: page_offset, limit: limit) BitbucketServer::Collection.new(paginator) - rescue *SERVER_ERRORS => e - raise ServerError, e end end end diff --git a/lib/bitbucket_server/connection.rb b/lib/bitbucket_server/connection.rb index 7efcdcf8619..9c14b26c65a 100644 --- a/lib/bitbucket_server/connection.rb +++ b/lib/bitbucket_server/connection.rb @@ -7,6 +7,17 @@ module BitbucketServer DEFAULT_API_VERSION = '1.0' SEPARATOR = '/' + NETWORK_ERRORS = [ + SocketError, + OpenSSL::SSL::SSLError, + Errno::ECONNRESET, + Errno::ECONNREFUSED, + Errno::EHOSTUNREACH, + Net::OpenTimeout, + Net::ReadTimeout, + Gitlab::HTTP::BlockedUrlError + ].freeze + attr_reader :api_version, :base_uri, :username, :token ConnectionError = Class.new(StandardError) @@ -27,6 +38,8 @@ module BitbucketServer check_errors!(response) response.parsed_response + rescue *NETWORK_ERRORS => e + raise ConnectionError, e end def post(path, body) @@ -38,6 +51,8 @@ module BitbucketServer check_errors!(response) response.parsed_response + rescue *NETWORK_ERRORS => e + raise ConnectionError, e end # We need to support two different APIs for deletion: @@ -55,6 +70,8 @@ module BitbucketServer check_errors!(response) response.parsed_response + rescue *NETWORK_ERRORS => e + raise ConnectionError, e end private diff --git a/lib/feature.rb b/lib/feature.rb index e048a443abc..e59cd70f822 100644 --- a/lib/feature.rb +++ b/lib/feature.rb @@ -102,4 +102,42 @@ class Feature expires_in: 1.hour) end end + + class Target + attr_reader :params + + def initialize(params) + @params = params + end + + def gate_specified? + %i(user project feature_group).any? { |key| params.key?(key) } + end + + def targets + [feature_group, user, project].compact + end + + private + + # rubocop: disable CodeReuse/ActiveRecord + def feature_group + return unless params.key?(:feature_group) + + Feature.group(params[:feature_group]) + end + # rubocop: enable CodeReuse/ActiveRecord + + def user + return unless params.key?(:user) + + UserFinder.new(params[:user]).find_by_username! + end + + def project + return unless params.key?(:project) + + Project.find_by_full_path(params[:project]) + end + end end diff --git a/lib/gitlab.rb b/lib/gitlab.rb index 2ef54658a11..b91394f7f58 100644 --- a/lib/gitlab.rb +++ b/lib/gitlab.rb @@ -27,12 +27,52 @@ module Gitlab end end + def self.version_info + Gitlab::VersionInfo.parse(Gitlab::VERSION) + end + COM_URL = 'https://gitlab.com'.freeze APP_DIRS_PATTERN = %r{^/?(app|config|ee|lib|spec|\(\w*\))} SUBDOMAIN_REGEX = %r{\Ahttps://[a-z0-9]+\.gitlab\.com\z} VERSION = File.read(root.join("VERSION")).strip.freeze INSTALLATION_TYPE = File.read(root.join("INSTALLATION_TYPE")).strip.freeze + def self.pre_release? + VERSION.include?('pre') + end + + def self.final_release? + !VERSION.include?('rc') && !pre_release? + end + + def self.minor_release + "#{version_info.major}.#{version_info.minor}" + end + + def self.prev_minor_release + "#{version_info.major}.#{version_info.minor - 1}" + end + + def self.prev_major_release + "#{version_info.major.to_i - 1}" + end + + def self.new_major_release? + version_info.minor.to_i.zero? + end + + def self.previous_release + if version_info.minor_version? + if version_info.patch_version? + minor_release + else + prev_minor_release + end + else + prev_major_release + end + end + def self.com? # Check `gl_subdomain?` as well to keep parity with gitlab.com Gitlab.config.gitlab.url == COM_URL || gl_subdomain? @@ -50,11 +90,11 @@ module Gitlab Rails.env.development? || org? || com? end - def self.pre_release? - VERSION.include?('pre') - end + def self.process_name + return 'sidekiq' if Sidekiq.server? + return 'console' if defined?(Rails::Console) + return 'test' if Rails.env.test? - def self.version_info - Gitlab::VersionInfo.parse(Gitlab::VERSION) + 'web' end end diff --git a/lib/gitlab/access/branch_protection.rb b/lib/gitlab/access/branch_protection.rb new file mode 100644 index 00000000000..f039e5c011f --- /dev/null +++ b/lib/gitlab/access/branch_protection.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +module Gitlab + module Access + # A wrapper around Integer based branch protection levels. + # + # This wrapper can be used to work with branch protection levels without + # having to directly refer to the constants. For example, instead of this: + # + # if access_level == Gitlab::Access::PROTECTION_DEV_CAN_PUSH + # ... + # end + # + # You can write this instead: + # + # protection = BranchProtection.new(access_level) + # + # if protection.developer_can_push? + # ... + # end + class BranchProtection + attr_reader :level + + # @param [Integer] level The branch protection level as an Integer. + def initialize(level) + @level = level + end + + def any? + level != PROTECTION_NONE + end + + def developer_can_push? + level == PROTECTION_DEV_CAN_PUSH + end + + def developer_can_merge? + level == PROTECTION_DEV_CAN_MERGE + end + end + end +end diff --git a/lib/gitlab/auth/ldap/person.rb b/lib/gitlab/auth/ldap/person.rb index a0244a3cea1..48d134f91b0 100644 --- a/lib/gitlab/auth/ldap/person.rb +++ b/lib/gitlab/auth/ldap/person.rb @@ -98,9 +98,7 @@ module Gitlab private - def entry - @entry - end + attr_reader :entry def config @config ||= Gitlab::Auth::LDAP::Config.new(provider) diff --git a/lib/gitlab/background_migration/backfill_project_repositories.rb b/lib/gitlab/background_migration/backfill_project_repositories.rb index aaf520d70f6..c8d83cc1803 100644 --- a/lib/gitlab/background_migration/backfill_project_repositories.rb +++ b/lib/gitlab/background_migration/backfill_project_repositories.rb @@ -83,7 +83,7 @@ module Gitlab extend ActiveSupport::Concern def full_path - @full_path ||= build_full_path + route&.path || build_full_path end def build_full_path @@ -99,7 +99,12 @@ module Gitlab end end - # Namespace model. + # Route model + class Route < ActiveRecord::Base + belongs_to :source, inverse_of: :route, polymorphic: true + end + + # Namespace model class Namespace < ActiveRecord::Base self.table_name = 'namespaces' self.inheritance_column = nil @@ -108,6 +113,8 @@ module Gitlab belongs_to :parent, class_name: 'Namespace', inverse_of: 'namespaces' + has_one :route, -> { where(source_type: 'Namespace') }, inverse_of: :source, foreign_key: :source_id + has_many :projects, inverse_of: :parent has_many :namespaces, inverse_of: :parent end @@ -134,6 +141,7 @@ module Gitlab belongs_to :parent, class_name: 'Namespace', foreign_key: :namespace_id, inverse_of: 'projects' + has_one :route, -> { where(source_type: 'Project') }, inverse_of: :source, foreign_key: :source_id has_one :project_repository, inverse_of: :project delegate :disk_path, to: :storage @@ -194,6 +202,8 @@ module Gitlab def project_repositories(start_id, stop_id) projects .without_project_repository + .includes(:route, parent: [:route]).references(:routes) + .includes(:parent).references(:namespaces) .where(id: start_id..stop_id) .map { |project| build_attributes_for_project(project) } .compact diff --git a/lib/gitlab/ci/config/entry/job.rb b/lib/gitlab/ci/config/entry/job.rb index 3239743bfff..1d8904f7b29 100644 --- a/lib/gitlab/ci/config/entry/job.rb +++ b/lib/gitlab/ci/config/entry/job.rb @@ -16,13 +16,6 @@ module Gitlab dependencies before_script after_script variables environment coverage retry parallel extends].freeze - DEFAULT_ONLY_POLICY = { - refs: %w(branches tags) - }.freeze - - DEFAULT_EXCEPT_POLICY = { - }.freeze - validations do validates :config, allowed_keys: ALLOWED_KEYS validates :config, presence: true @@ -73,7 +66,8 @@ module Gitlab description: 'Services that will be used to execute this job.' entry :only, Entry::Policy, - description: 'Refs policy this job will be executed for.' + description: 'Refs policy this job will be executed for.', + default: { refs: %w[branches tags] } entry :except, Entry::Policy, description: 'Refs policy this job will be executed for.' @@ -156,8 +150,8 @@ module Gitlab services: services_value, stage: stage_value, cache: cache_value, - only: DEFAULT_ONLY_POLICY.deep_merge(only_value.to_h), - except: DEFAULT_EXCEPT_POLICY.deep_merge(except_value.to_h), + only: only_value, + except: except_value, variables: variables_defined? ? variables_value : nil, environment: environment_defined? ? environment_value : nil, environment_name: environment_defined? ? environment_value[:name] : nil, diff --git a/lib/gitlab/ci/config/entry/policy.rb b/lib/gitlab/ci/config/entry/policy.rb index 998da1f6837..9c677bf6617 100644 --- a/lib/gitlab/ci/config/entry/policy.rb +++ b/lib/gitlab/ci/config/entry/policy.rb @@ -64,7 +64,8 @@ module Gitlab end end - def self.default + def value + default.to_h.deep_merge(subject.value.to_h) end end end diff --git a/lib/gitlab/ci/config/entry/retry.rb b/lib/gitlab/ci/config/entry/retry.rb index eaf8b38aa3c..e9cbcb31e21 100644 --- a/lib/gitlab/ci/config/entry/retry.rb +++ b/lib/gitlab/ci/config/entry/retry.rb @@ -82,9 +82,6 @@ module Gitlab 'retry config' end end - - def self.default - end end end end diff --git a/lib/gitlab/ci/config/entry/variables.rb b/lib/gitlab/ci/config/entry/variables.rb index 89d790ebfa6..c9d0c7cb568 100644 --- a/lib/gitlab/ci/config/entry/variables.rb +++ b/lib/gitlab/ci/config/entry/variables.rb @@ -14,7 +14,7 @@ module Gitlab validates :config, variables: true end - def self.default + def self.default(**) {} end diff --git a/lib/gitlab/ci/config/normalizer.rb b/lib/gitlab/ci/config/normalizer.rb index b7743bd2090..191f5d09645 100644 --- a/lib/gitlab/ci/config/normalizer.rb +++ b/lib/gitlab/ci/config/normalizer.rb @@ -46,7 +46,8 @@ module Gitlab parallelized_job_names = @parallelized_jobs.keys.map(&:to_s) parallelized_config.each_with_object({}) do |(job_name, config), hash| if config[:dependencies] && (intersection = config[:dependencies] & parallelized_job_names).any? - deps = intersection.map { |dep| @parallelized_jobs[dep.to_sym].map(&:first) }.flatten + parallelized_deps = intersection.map { |dep| @parallelized_jobs[dep.to_sym].map(&:first) }.flatten + deps = config[:dependencies] - intersection + parallelized_deps hash[job_name] = config.merge(dependencies: deps) else hash[job_name] = config diff --git a/lib/gitlab/ci/cron_parser.rb b/lib/gitlab/ci/cron_parser.rb index b1db9084662..94f4a4e36c9 100644 --- a/lib/gitlab/ci/cron_parser.rb +++ b/lib/gitlab/ci/cron_parser.rb @@ -35,7 +35,7 @@ module Gitlab # NOTE: # cron_timezone can only accept timezones listed in TZInfo::Timezone. # Aliases of Timezones from ActiveSupport::TimeZone are NOT accepted, - # because Rufus::Scheduler only supports TZInfo::Timezone. + # because Fugit::Cron only supports TZInfo::Timezone. # # For example, those codes have the same effect. # Time.zone = 'Pacific Time (US & Canada)' (ActiveSupport::TimeZone) @@ -47,10 +47,7 @@ module Gitlab # If you want to know more, please take a look # https://github.com/rails/rails/blob/master/activesupport/lib/active_support/values/time_zone.rb def try_parse_cron(cron, cron_timezone) - cron_line = Rufus::Scheduler.parse("#{cron} #{cron_timezone}") - cron_line if cron_line.is_a?(Rufus::Scheduler::CronLine) - rescue - # noop + Fugit::Cron.parse("#{cron} #{cron_timezone}") end end end diff --git a/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml b/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml index e292641da2b..95160e1432f 100644 --- a/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml @@ -49,7 +49,7 @@ variables: POSTGRES_ENABLED: "true" POSTGRES_DB: $CI_ENVIRONMENT_SLUG - KUBERNETES_VERSION: 1.10.9 + KUBERNETES_VERSION: 1.11.6 HELM_VERSION: 2.11.0 DOCKER_DRIVER: overlay2 @@ -667,6 +667,14 @@ rollout 100%: create_application_secret "$track" + env_slug=$(echo ${CI_ENVIRONMENT_SLUG//-/_} | tr -s '[:lower:]' '[:upper:]') + eval env_ADDITIONAL_HOSTS=\$${env_slug}_ADDITIONAL_HOSTS + if [ -n "$env_ADDITIONAL_HOSTS" ]; then + additional_hosts="{$env_ADDITIONAL_HOSTS}" + elif [ -n "$ADDITIONAL_HOSTS" ]; then + additional_hosts="{$ADDITIONAL_HOSTS}" + fi + if [[ -n "$DB_INITIALIZE" && -z "$(helm ls -q "^$name$")" ]]; then echo "Deploying first release with database initialization..." helm upgrade --install \ @@ -681,6 +689,7 @@ rollout 100%: --set application.database_url="$DATABASE_URL" \ --set application.secretName="$APPLICATION_SECRET_NAME" \ --set service.url="$CI_ENVIRONMENT_URL" \ + --set service.additionalHosts="$additional_hosts" \ --set replicaCount="$replicas" \ --set postgresql.enabled="$postgres_enabled" \ --set postgresql.nameOverride="postgres" \ @@ -714,6 +723,7 @@ rollout 100%: --set application.database_url="$DATABASE_URL" \ --set application.secretName="$APPLICATION_SECRET_NAME" \ --set service.url="$CI_ENVIRONMENT_URL" \ + --set service.additionalHosts="$additional_hosts" \ --set replicaCount="$replicas" \ --set postgresql.enabled="$postgres_enabled" \ --set postgresql.nameOverride="postgres" \ diff --git a/lib/gitlab/config/entry/configurable.rb b/lib/gitlab/config/entry/configurable.rb index afdb60b2cd5..37ba16dba25 100644 --- a/lib/gitlab/config/entry/configurable.rb +++ b/lib/gitlab/config/entry/configurable.rb @@ -56,6 +56,7 @@ module Gitlab def entry(key, entry, metadata) factory = ::Gitlab::Config::Entry::Factory.new(entry) .with(description: metadata[:description]) + .with(default: metadata[:default]) (@nodes ||= {}).merge!(key.to_sym => factory) end diff --git a/lib/gitlab/config/entry/factory.rb b/lib/gitlab/config/entry/factory.rb index 30d43c9f9a1..79f9ff32514 100644 --- a/lib/gitlab/config/entry/factory.rb +++ b/lib/gitlab/config/entry/factory.rb @@ -12,7 +12,7 @@ module Gitlab def initialize(entry) @entry = entry @metadata = {} - @attributes = {} + @attributes = { default: entry.default } end def value(value) @@ -21,12 +21,12 @@ module Gitlab end def metadata(metadata) - @metadata.merge!(metadata) + @metadata.merge!(metadata.compact) self end def with(attributes) - @attributes.merge!(attributes) + @attributes.merge!(attributes.compact) self end @@ -38,9 +38,7 @@ module Gitlab # See issue #18775. # if @value.nil? - Entry::Unspecified.new( - fabricate_unspecified - ) + Entry::Unspecified.new(fabricate_unspecified) else fabricate(@entry, @value) end @@ -53,10 +51,12 @@ module Gitlab # If entry has a default value we fabricate concrete node # with default value. # - if @entry.default.nil? + default = @attributes.fetch(:default) + + if default.nil? fabricate(Entry::Undefined) else - fabricate(@entry, @entry.default) + fabricate(@entry, default) end end @@ -64,6 +64,7 @@ module Gitlab entry.new(value, @metadata).tap do |node| node.key = @attributes[:key] node.parent = @attributes[:parent] + node.default = @attributes[:default] node.description = @attributes[:description] end end diff --git a/lib/gitlab/config/entry/node.rb b/lib/gitlab/config/entry/node.rb index 30357b2c95b..9999ab4ff95 100644 --- a/lib/gitlab/config/entry/node.rb +++ b/lib/gitlab/config/entry/node.rb @@ -10,7 +10,7 @@ module Gitlab InvalidError = Class.new(StandardError) attr_reader :config, :metadata - attr_accessor :key, :parent, :description + attr_accessor :key, :parent, :default, :description def initialize(config, **metadata) @config = config @@ -85,7 +85,7 @@ module Gitlab "#<#{self.class.name} #{unspecified}{#{key}: #{val.inspect}}>" end - def self.default + def self.default(**) end def self.aspects diff --git a/lib/gitlab/config/entry/simplifiable.rb b/lib/gitlab/config/entry/simplifiable.rb index 3e148fe2e91..5fbf7565e2a 100644 --- a/lib/gitlab/config/entry/simplifiable.rb +++ b/lib/gitlab/config/entry/simplifiable.rb @@ -6,6 +6,8 @@ module Gitlab class Simplifiable < SimpleDelegator EntryStrategy = Struct.new(:name, :condition) + attr_reader :subject + def initialize(config, **metadata) unless self.class.const_defined?(:UnknownStrategy) raise ArgumentError, 'UndefinedStrategy not available!' @@ -17,7 +19,7 @@ module Gitlab entry = self.class.entry_class(strategy) - super(entry.new(config, metadata)) + super(@subject = entry.new(config, metadata)) end def self.strategy(name, **opts) @@ -37,6 +39,9 @@ module Gitlab self::UnknownStrategy end end + + def self.default + end end end end diff --git a/lib/gitlab/error_tracking/error.rb b/lib/gitlab/error_tracking/error.rb new file mode 100644 index 00000000000..4af5192aa6a --- /dev/null +++ b/lib/gitlab/error_tracking/error.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module Gitlab + module ErrorTracking + class Error + include ActiveModel::Model + + attr_accessor :id, :title, :type, :user_count, :count, + :first_seen, :last_seen, :message, :culprit, + :external_url, :project_id, :project_name, :project_slug, + :short_id, :status, :frequency + end + end +end diff --git a/lib/gitlab/etag_caching/middleware.rb b/lib/gitlab/etag_caching/middleware.rb index 0341f930b9c..a11d6b66409 100644 --- a/lib/gitlab/etag_caching/middleware.rb +++ b/lib/gitlab/etag_caching/middleware.rb @@ -8,7 +8,7 @@ module Gitlab end def call(env) - request = Rack::Request.new(env) + request = ActionDispatch::Request.new(env) route = Gitlab::EtagCaching::Router.match(request.path_info) return @app.call(env) unless route diff --git a/lib/gitlab/git/bundle_file.rb b/lib/gitlab/git/bundle_file.rb new file mode 100644 index 00000000000..8384a436fcc --- /dev/null +++ b/lib/gitlab/git/bundle_file.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +module Gitlab + module Git + class BundleFile + # All git bundle files start with this string + # + # https://github.com/git/git/blob/v2.20.1/bundle.c#L15 + MAGIC = "# v2 git bundle\n" + + InvalidBundleError = Class.new(StandardError) + + attr_reader :filename + + def self.check!(filename) + new(filename).check! + end + + def initialize(filename) + @filename = filename + end + + def check! + data = File.open(filename, 'r') { |f| f.read(MAGIC.size) } + + raise InvalidBundleError, 'Invalid bundle file' unless data == MAGIC + end + end + end +end diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb index 5bbedc9d5e3..786c90f9272 100644 --- a/lib/gitlab/git/repository.rb +++ b/lib/gitlab/git/repository.rb @@ -789,6 +789,11 @@ module Gitlab end def create_from_bundle(bundle_path) + # It's important to check that the linked-to file is actually a valid + # .bundle file as it is passed to `git clone`, which may otherwise + # interpret it as a pointer to another repository + ::Gitlab::Git::BundleFile.check!(bundle_path) + gitaly_repository_client.create_from_bundle(bundle_path) end diff --git a/lib/gitlab/gon_helper.rb b/lib/gitlab/gon_helper.rb index 15137140639..9b1794eec91 100644 --- a/lib/gitlab/gon_helper.rb +++ b/lib/gitlab/gon_helper.rb @@ -8,10 +8,7 @@ module Gitlab def add_gon_variables gon.api_version = 'v4' - gon.default_avatar_url = - Gitlab::Utils.append_path( - Gitlab.config.gitlab.url, - ActionController::Base.helpers.image_path('no_avatar.png')) + gon.default_avatar_url = default_avatar_url gon.max_file_size = Gitlab::CurrentSettings.max_attachment_size gon.asset_host = ActionController::Base.asset_host gon.webpack_public_path = webpack_public_path @@ -50,5 +47,15 @@ module Gitlab # use this method to push multiple feature flags. gon.push({ features: { var_name => enabled } }, true) end + + def default_avatar_url + # We can't use ActionController::Base.helpers.image_url because it + # doesn't return an actual URL because request is nil for some reason. + # + # We also can't use Gitlab::Utils.append_path because the image path + # may be an absolute URL. + URI.join(Gitlab.config.gitlab.url, + ActionController::Base.helpers.image_path('no_avatar.png')).to_s + end end end diff --git a/lib/gitlab/middleware/basic_health_check.rb b/lib/gitlab/middleware/basic_health_check.rb index f2a03217098..acf8c301b8f 100644 --- a/lib/gitlab/middleware/basic_health_check.rb +++ b/lib/gitlab/middleware/basic_health_check.rb @@ -24,7 +24,7 @@ module Gitlab def call(env) return @app.call(env) unless env['PATH_INFO'] == HEALTH_PATH - request = Rack::Request.new(env) + request = ActionDispatch::Request.new(env) return OK_RESPONSE if client_ip_whitelisted?(request) diff --git a/lib/gitlab/middleware/read_only/controller.rb b/lib/gitlab/middleware/read_only/controller.rb index 89941a9efa0..817db12ac55 100644 --- a/lib/gitlab/middleware/read_only/controller.rb +++ b/lib/gitlab/middleware/read_only/controller.rb @@ -60,7 +60,7 @@ module Gitlab end def request - @env['rack.request'] ||= Rack::Request.new(@env) + @env['actionpack.request'] ||= ActionDispatch::Request.new(@env) end def last_visited_url @@ -71,12 +71,16 @@ module Gitlab @route_hash ||= Rails.application.routes.recognize_path(request.url, { method: request.request_method }) rescue {} end + def relative_url + File.join('', Gitlab.config.gitlab.relative_url_root).chomp('/') + end + # Overridden in EE module def whitelisted_routes - grack_route || ReadOnly.internal_routes.any? { |path| request.path.include?(path) } || lfs_route || sidekiq_route + grack_route? || internal_route? || lfs_route? || sidekiq_route? end - def grack_route + def grack_route? # Calling route_hash may be expensive. Only do it if we think there's a possible match return false unless request.path.end_with?('.git/git-upload-pack', '.git/git-receive-pack') @@ -84,7 +88,11 @@ module Gitlab WHITELISTED_GIT_ROUTES[route_hash[:controller]]&.include?(route_hash[:action]) end - def lfs_route + def internal_route? + ReadOnly.internal_routes.any? { |path| request.path.include?(path) } + end + + def lfs_route? # Calling route_hash may be expensive. Only do it if we think there's a possible match unless request.path.end_with?('/info/lfs/objects/batch', '/info/lfs/locks', '/info/lfs/locks/verify') || @@ -95,8 +103,8 @@ module Gitlab WHITELISTED_GIT_LFS_ROUTES[route_hash[:controller]]&.include?(route_hash[:action]) end - def sidekiq_route - request.path.start_with?('/admin/sidekiq') + def sidekiq_route? + request.path.start_with?("#{relative_url}/admin/sidekiq") end end end diff --git a/lib/gitlab/release_blog_post.rb b/lib/gitlab/release_blog_post.rb new file mode 100644 index 00000000000..639aee61464 --- /dev/null +++ b/lib/gitlab/release_blog_post.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true +require 'singleton' + +module Gitlab + class ReleaseBlogPost + include Singleton + + RELEASE_RSS_URL = 'https://about.gitlab.com/releases.xml' + + def blog_post_url + @url ||= fetch_blog_post_url + end + + private + + def fetch_blog_post_url + installed_version = Gitlab.final_release? ? Gitlab.minor_release : Gitlab.previous_release + response = Gitlab::HTTP.get(RELEASE_RSS_URL, verify: false) + + return unless response.code == 200 + + blog_entry = find_installed_blog_entry(response, installed_version) + blog_entry['id'] if blog_entry + end + + def find_installed_blog_entry(response, installed_version) + response['feed']['entry'].find do |entry| + entry['release'] == installed_version || matches_previous_release_post(entry['release'], installed_version) + end + end + + def should_match_previous_release_post? + Gitlab.new_major_release? && !Gitlab.final_release? + end + + def matches_previous_release_post(rss_release_version, installed_version) + should_match_previous_release_post? && rss_release_version[/\d+/] == installed_version + end + end +end diff --git a/lib/gitlab/repository_cache.rb b/lib/gitlab/repository_cache.rb index 6b0808f5304..56007574b1b 100644 --- a/lib/gitlab/repository_cache.rb +++ b/lib/gitlab/repository_cache.rb @@ -7,13 +7,13 @@ module Gitlab def initialize(repository, extra_namespace: nil, backend: Rails.cache) @repository = repository - @namespace = "project:#{repository.project.id}" + @namespace = "#{repository.full_path}:#{repository.project.id}" @namespace = "#{@namespace}:#{extra_namespace}" if extra_namespace @backend = backend end def cache_key(type) - "#{namespace}:#{type}" + "#{type}:#{namespace}" end def expire(key) diff --git a/lib/gitlab/request_context.rb b/lib/gitlab/request_context.rb index f8f8ec789ce..d9811e036d3 100644 --- a/lib/gitlab/request_context.rb +++ b/lib/gitlab/request_context.rb @@ -13,7 +13,7 @@ module Gitlab end def call(env) - req = Rack::Request.new(env) + req = ActionDispatch::Request.new(env) Gitlab::SafeRequestStore[:client_ip] = req.ip diff --git a/lib/gitlab/seeder.rb b/lib/gitlab/seeder.rb index 84a51773276..8e2f16271eb 100644 --- a/lib/gitlab/seeder.rb +++ b/lib/gitlab/seeder.rb @@ -26,6 +26,19 @@ module Gitlab puts "\nOK".color(:green) end + def self.without_gitaly_timeout + # Remove Gitaly timeout + old_timeout = Gitlab::CurrentSettings.current_application_settings.gitaly_timeout_default + Gitlab::CurrentSettings.current_application_settings.update_columns(gitaly_timeout_default: 0) + # Otherwise we still see the default value when running seed_fu + ApplicationSetting.expire + + yield + ensure + Gitlab::CurrentSettings.current_application_settings.update_columns(gitaly_timeout_default: old_timeout) + ApplicationSetting.expire + end + def self.mute_notifications NotificationService.prepend(MuteNotifications) end diff --git a/lib/gitlab/sentry.rb b/lib/gitlab/sentry.rb index 46d01964eac..956c16117f5 100644 --- a/lib/gitlab/sentry.rb +++ b/lib/gitlab/sentry.rb @@ -52,14 +52,6 @@ module Gitlab end end - def self.program_context - if Sidekiq.server? - 'sidekiq' - else - 'rails' - end - end - def self.should_raise_for_dev? Rails.env.development? || Rails.env.test? end diff --git a/lib/gitlab/tracing.rb b/lib/gitlab/tracing.rb new file mode 100644 index 00000000000..3c4db42ac06 --- /dev/null +++ b/lib/gitlab/tracing.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module Gitlab + module Tracing + # Only enable tracing when the `GITLAB_TRACING` env var is configured. Note that we avoid using ApplicationSettings since + # the same environment variable needs to be configured for Workhorse, Gitaly and any other components which + # emit tracing. Since other components may start before Rails, and may not have access to ApplicationSettings, + # an env var makes more sense. + def self.enabled? + connection_string.present? + end + + def self.connection_string + ENV['GITLAB_TRACING'] + end + end +end diff --git a/lib/gitlab/tracing/factory.rb b/lib/gitlab/tracing/factory.rb new file mode 100644 index 00000000000..fc714164353 --- /dev/null +++ b/lib/gitlab/tracing/factory.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +require "cgi" + +module Gitlab + module Tracing + class Factory + OPENTRACING_SCHEME = "opentracing" + + def self.create_tracer(service_name, connection_string) + return unless connection_string.present? + + begin + opentracing_details = parse_connection_string(connection_string) + driver_name = opentracing_details[:driver_name] + + case driver_name + when "jaeger" + JaegerFactory.create_tracer(service_name, opentracing_details[:options]) + else + raise "Unknown driver: #{driver_name}" + end + rescue => e + # Can't create the tracer? Warn and continue sans tracer + warn "Unable to instantiate tracer: #{e}" + nil + end + end + + def self.parse_connection_string(connection_string) + parsed = URI.parse(connection_string) + + unless valid_uri?(parsed) + raise "Invalid tracing connection string" + end + + { + driver_name: parsed.host, + options: parse_query(parsed.query) + } + end + private_class_method :parse_connection_string + + def self.parse_query(query) + return {} unless query + + CGI.parse(query).symbolize_keys.transform_values(&:first) + end + private_class_method :parse_query + + def self.valid_uri?(uri) + return false unless uri + + uri.scheme == OPENTRACING_SCHEME && + uri.host.to_s =~ /^[a-z0-9_]+$/ && + uri.path.empty? + end + private_class_method :valid_uri? + end + end +end diff --git a/lib/gitlab/tracing/jaeger_factory.rb b/lib/gitlab/tracing/jaeger_factory.rb new file mode 100644 index 00000000000..0726f6b67f4 --- /dev/null +++ b/lib/gitlab/tracing/jaeger_factory.rb @@ -0,0 +1,97 @@ +# frozen_string_literal: true + +require 'jaeger/client' + +module Gitlab + module Tracing + class JaegerFactory + # When the probabilistic sampler is used, by default 0.1% of requests will be traced + DEFAULT_PROBABILISTIC_RATE = 0.001 + + # The default port for the Jaeger agent UDP listener + DEFAULT_UDP_PORT = 6831 + + # Reduce this from default of 10 seconds as the Ruby jaeger + # client doesn't have overflow control, leading to very large + # messages which fail to send over UDP (max packet = 64k) + # Flush more often, with smaller packets + FLUSH_INTERVAL = 5 + + def self.create_tracer(service_name, options) + kwargs = { + service_name: service_name, + sampler: get_sampler(options[:sampler], options[:sampler_param]), + reporter: get_reporter(service_name, options[:http_endpoint], options[:udp_endpoint]) + } + + extra_params = options.except(:sampler, :sampler_param, :http_endpoint, :udp_endpoint, :strict_parsing, :debug) # rubocop: disable CodeReuse/ActiveRecord + if extra_params.present? + message = "jaeger tracer: invalid option: #{extra_params.keys.join(", ")}" + + if options[:strict_parsing] + raise message + else + warn message + end + end + + Jaeger::Client.build(kwargs) + end + + def self.get_sampler(sampler_type, sampler_param) + case sampler_type + when "probabilistic" + sampler_rate = sampler_param ? sampler_param.to_f : DEFAULT_PROBABILISTIC_RATE + Jaeger::Samplers::Probabilistic.new(rate: sampler_rate) + when "const" + const_value = sampler_param == "1" + Jaeger::Samplers::Const.new(const_value) + else + nil + end + end + private_class_method :get_sampler + + def self.get_reporter(service_name, http_endpoint, udp_endpoint) + encoder = Jaeger::Encoders::ThriftEncoder.new(service_name: service_name) + + if http_endpoint.present? + sender = get_http_sender(encoder, http_endpoint) + elsif udp_endpoint.present? + sender = get_udp_sender(encoder, udp_endpoint) + else + return nil + end + + Jaeger::Reporters::RemoteReporter.new( + sender: sender, + flush_interval: FLUSH_INTERVAL + ) + end + private_class_method :get_reporter + + def self.get_http_sender(encoder, address) + Jaeger::HttpSender.new( + url: address, + encoder: encoder, + logger: Logger.new(STDOUT) + ) + end + private_class_method :get_http_sender + + def self.get_udp_sender(encoder, address) + pair = address.split(":", 2) + host = pair[0] + port = pair[1] ? pair[1].to_i : DEFAULT_UDP_PORT + + Jaeger::UdpSender.new( + host: host, + port: port, + encoder: encoder, + logger: Logger.new(STDOUT) + ) + end + private_class_method :get_udp_sender + end + end +end diff --git a/lib/gitlab/version_info.rb b/lib/gitlab/version_info.rb index aa6d5310161..142ead12c08 100644 --- a/lib/gitlab/version_info.rb +++ b/lib/gitlab/version_info.rb @@ -20,6 +20,14 @@ module Gitlab @patch = patch end + def minor_version? + minor.to_i > 0 + end + + def patch_version? + patch.to_i > 0 + end + def <=>(other) return unless other.is_a? VersionInfo return unless valid? && other.valid? diff --git a/lib/sentry/client.rb b/lib/sentry/client.rb new file mode 100644 index 00000000000..343f2c49a7f --- /dev/null +++ b/lib/sentry/client.rb @@ -0,0 +1,104 @@ +# frozen_string_literal: true + +module Sentry + class Client + Error = Class.new(StandardError) + + attr_accessor :url, :token + + def initialize(api_url, token) + @url = api_url + @token = token + end + + def list_issues(issue_status:, limit:) + issues = get_issues(issue_status: issue_status, limit: limit) + map_to_errors(issues) + end + + private + + def request_params + { + headers: { + 'Authorization' => "Bearer #{@token}" + }, + follow_redirects: false + } + end + + def get_issues(issue_status:, limit:) + resp = Gitlab::HTTP.get( + issues_api_url, + **request_params.merge(query: { + query: "is:#{issue_status}", + limit: limit + }) + ) + + handle_response(resp) + end + + def handle_response(response) + unless response.code == 200 + raise Client::Error, "Sentry response error: #{response.code}" + end + + response.as_json + end + + def issues_api_url + issues_url = URI(@url + '/issues/') + issues_url.path.squeeze!('/') + + issues_url + end + + def map_to_errors(issues) + issues.map do |issue| + map_to_error(issue) + end + end + + def issue_url(id) + issues_url = @url + "/issues/#{id}" + issues_url = ErrorTracking::ProjectErrorTrackingSetting.extract_sentry_external_url(issues_url) + + uri = URI(issues_url) + uri.path.squeeze!('/') + + uri.to_s + end + + def map_to_error(issue) + id = issue.fetch('id') + project = issue.fetch('project') + + count = issue.fetch('count', nil) + + frequency = issue.dig('stats', '24h') + message = issue.dig('metadata', 'value') + + external_url = issue_url(id) + + Gitlab::ErrorTracking::Error.new( + id: id, + first_seen: issue.fetch('firstSeen', nil), + last_seen: issue.fetch('lastSeen', nil), + title: issue.fetch('title', nil), + type: issue.fetch('type', nil), + user_count: issue.fetch('userCount', nil), + count: count, + message: message, + culprit: issue.fetch('culprit', nil), + external_url: external_url, + short_id: issue.fetch('shortId', nil), + status: issue.fetch('status', nil), + frequency: frequency, + project_id: project.fetch('id'), + project_name: project.fetch('name', nil), + project_slug: project.fetch('slug', nil) + ) + end + end +end diff --git a/lib/system_check/base_check.rb b/lib/system_check/base_check.rb index e06245294c4..46aad8aa885 100644 --- a/lib/system_check/base_check.rb +++ b/lib/system_check/base_check.rb @@ -70,18 +70,14 @@ module SystemCheck # multiple reasons why a check can fail # # @param [String] reason to be displayed - def skip_reason=(reason) - @skip_reason = reason - end + attr_writer :skip_reason # Skip reason defined during runtime # # This value have precedence over the one defined in the subclass # # @return [String] the reason - def skip_reason - @skip_reason - end + attr_reader :skip_reason # Does the check support automatically repair routine? # diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 2f8a1c8b03d..795a46382a7 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -129,6 +129,9 @@ msgstr "" msgid "%{issuableType} will be removed! Are you sure?" msgstr "" +msgid "%{link_start}Read more%{link_end} about role permissions" +msgstr "" + msgid "%{loadingIcon} Started" msgstr "" @@ -399,6 +402,9 @@ msgstr "" msgid "Add reaction" msgstr "" +msgid "Add to project" +msgstr "" + msgid "Add todo" msgstr "" @@ -450,6 +456,9 @@ msgstr "" msgid "AdminProjects|Delete project" msgstr "" +msgid "AdminSettings|Auto DevOps domain" +msgstr "" + msgid "AdminSettings|Environment variables are protected by default" msgstr "" @@ -873,10 +882,7 @@ msgstr "" msgid "Available" msgstr "" -msgid "Available group Runners : %{runners}" -msgstr "" - -msgid "Available group Runners : %{runners}." +msgid "Available group Runners: %{runners}" msgstr "" msgid "Available shared Runners:" @@ -1305,6 +1311,9 @@ msgstr "" msgid "Choose a file" msgstr "" +msgid "Choose a role permission" +msgstr "" + msgid "Choose a template..." msgstr "" @@ -1464,6 +1473,9 @@ msgstr "" msgid "Closed" msgstr "" +msgid "ClusterIntegration| is the default environment scope for this cluster. This means that all jobs, regardless of their environment, will use this cluster. %{environment_scope_start}More information%{environment_scope_end}" +msgstr "" + msgid "ClusterIntegration|%{appList} was successfully installed on your Kubernetes cluster" msgstr "" @@ -1890,6 +1902,9 @@ msgstr "" msgid "ClusterIntegration|sign up" msgstr "" +msgid "Code" +msgstr "" + msgid "Cohorts" msgstr "" @@ -1928,9 +1943,15 @@ msgid_plural "Commits" msgstr[0] "" msgstr[1] "" +msgid "Commit %{commit_id}" +msgstr "" + msgid "Commit Message" msgstr "" +msgid "Commit deleted" +msgstr "" + msgid "Commit duration in minutes for last 30 commits" msgstr "" @@ -2749,6 +2770,9 @@ msgstr "" msgid "Enable and configure Prometheus metrics." msgstr "" +msgid "Enable error tracking" +msgstr "" + msgid "Enable for this project" msgstr "" @@ -2767,6 +2791,9 @@ msgstr "" msgid "Enable the Performance Bar for a given group." msgstr "" +msgid "Enable two-factor authentication" +msgstr "" + msgid "Enable usage ping" msgstr "" @@ -2944,6 +2971,9 @@ msgstr "" msgid "Error occurred when toggling the notification subscription" msgstr "" +msgid "Error rendering markdown preview" +msgstr "" + msgid "Error saving label update." msgstr "" @@ -2956,6 +2986,9 @@ msgstr "" msgid "Error while loading the merge request. Please try again." msgstr "" +msgid "Errors" +msgstr "" + msgid "Estimated" msgstr "" @@ -2977,6 +3010,9 @@ msgstr "" msgid "EventFilterBy|Filter by team" msgstr "" +msgid "Events" +msgstr "" + msgid "Every %{action} attempt has failed: %{job_error_message}. Please try again." msgstr "" @@ -3001,6 +3037,9 @@ msgstr "" msgid "Existing folder" msgstr "" +msgid "Existing members and groups" +msgstr "" + msgid "Expand" msgstr "" @@ -3064,6 +3103,9 @@ msgstr "" msgid "Failed to load emoji list." msgstr "" +msgid "Failed to load errors from Sentry" +msgstr "" + msgid "Failed to remove issue from board, please try again." msgstr "" @@ -3124,6 +3166,12 @@ msgstr "" msgid "Filter by two-factor authentication" msgstr "" +msgid "Filter results by group" +msgstr "" + +msgid "Filter results by project" +msgstr "" + msgid "Filter..." msgstr "" @@ -3133,6 +3181,9 @@ msgstr "" msgid "Find by path" msgstr "" +msgid "Find existing members by name" +msgstr "" + msgid "Find file" msgstr "" @@ -3247,6 +3298,9 @@ msgstr "" msgid "Geo" msgstr "" +msgid "Get started with error tracking" +msgstr "" + msgid "Getting started with releases" msgstr "" @@ -3382,6 +3436,9 @@ msgstr "" msgid "Group name" msgstr "" +msgid "Group:" +msgstr "" + msgid "Group: %{group_name}" msgstr "" @@ -3421,6 +3478,9 @@ msgstr "" msgid "Groups can also be nested by creating %{subgroup_docs_link_start}subgroups%{subgroup_docs_link_end}." msgstr "" +msgid "Groups with access to <strong>%{project_name}</strong>" +msgstr "" + msgid "GroupsDropdown|Frequently visited" msgstr "" @@ -3648,12 +3708,21 @@ msgstr "" msgid "Import issues" msgstr "" +msgid "Import members" +msgstr "" + +msgid "Import members from another project" +msgstr "" + msgid "Import multiple repositories by uploading a manifest file." msgstr "" msgid "Import project" msgstr "" +msgid "Import project members" +msgstr "" + msgid "Import projects from Bitbucket" msgstr "" @@ -3759,6 +3828,12 @@ msgstr "" msgid "Invite" msgstr "" +msgid "Invite group" +msgstr "" + +msgid "Invite member" +msgstr "" + msgid "Invoke Count" msgstr "" @@ -3953,6 +4028,9 @@ msgstr "" msgid "Last reply by" msgstr "" +msgid "Last seen" +msgstr "" + msgid "Last update" msgstr "" @@ -4096,6 +4174,9 @@ msgstr "" msgid "Manage project labels" msgstr "" +msgid "Manage two-factor authentication" +msgstr "" + msgid "Manifest" msgstr "" @@ -4177,6 +4258,12 @@ msgstr "" msgid "Members" msgstr "" +msgid "Members can be added by project <i>Maintainers</i> or <i>Owners</i>" +msgstr "" + +msgid "Members of <strong>%{project_name}</strong>" +msgstr "" + msgid "Merge Request" msgstr "" @@ -4342,6 +4429,9 @@ msgstr "" msgid "Modal|Close" msgstr "" +msgid "Monitor your errors by integrating with Sentry" +msgstr "" + msgid "Monitoring" msgstr "" @@ -4506,9 +4596,15 @@ msgstr "" msgid "No contributions were found" msgstr "" +msgid "No details available" +msgstr "" + msgid "No due date" msgstr "" +msgid "No errors to display" +msgstr "" + msgid "No estimate or time spent" msgstr "" @@ -4715,6 +4811,9 @@ msgstr "" msgid "Only project members can comment." msgstr "" +msgid "Only project members will be imported. Group members will be skipped." +msgstr "" + msgid "Oops, are you sure?" msgstr "" @@ -4727,6 +4826,9 @@ msgstr "" msgid "Open comment type dropdown" msgstr "" +msgid "Open errors" +msgstr "" + msgid "Open in Xcode" msgstr "" @@ -4835,6 +4937,9 @@ msgstr "" msgid "Personal Access Token" msgstr "" +msgid "Personal project creation is not allowed. Please contact your administrator with questions" +msgstr "" + msgid "Pick a name" msgstr "" @@ -5114,6 +5219,12 @@ msgstr "" msgid "Profiles|Account scheduled for removal." msgstr "" +msgid "Profiles|Activate signin with one of the following services" +msgstr "" + +msgid "Profiles|Active" +msgstr "" + msgid "Profiles|Add key" msgstr "" @@ -5129,6 +5240,9 @@ msgstr "" msgid "Profiles|Change username" msgstr "" +msgid "Profiles|Changing your username can have unintended side effects." +msgstr "" + msgid "Profiles|Choose file..." msgstr "" @@ -5141,6 +5255,15 @@ msgstr "" msgid "Profiles|Clear status" msgstr "" +msgid "Profiles|Click on icon to activate signin with one of the following services" +msgstr "" + +msgid "Profiles|Connect" +msgstr "" + +msgid "Profiles|Connected Accounts" +msgstr "" + msgid "Profiles|Current path: %{path}" msgstr "" @@ -5159,6 +5282,9 @@ msgstr "" msgid "Profiles|Deleting an account has the following effects:" msgstr "" +msgid "Profiles|Disconnect" +msgstr "" + msgid "Profiles|Do not show on profile" msgstr "" @@ -5171,6 +5297,9 @@ msgstr "" msgid "Profiles|Enter your name, so people you know can recognize you" msgstr "" +msgid "Profiles|Increase your account's security by enabling Two-Factor Authentication (2FA)" +msgstr "" + msgid "Profiles|Invalid password" msgstr "" @@ -5207,6 +5336,9 @@ msgstr "" msgid "Profiles|Set new profile picture" msgstr "" +msgid "Profiles|Social sign-in" +msgstr "" + msgid "Profiles|Some options are unavailable for LDAP accounts" msgstr "" @@ -5234,6 +5366,9 @@ msgstr "" msgid "Profiles|This information will appear on your profile" msgstr "" +msgid "Profiles|Two-Factor Authentication" +msgstr "" + msgid "Profiles|Type your %{confirmationValue} to confirm:" msgstr "" @@ -5366,12 +5501,18 @@ msgstr "" msgid "Project export started. A download link will be sent by email." msgstr "" +msgid "Project members" +msgstr "" + msgid "Project name" msgstr "" msgid "Project slug" msgstr "" +msgid "Project:" +msgstr "" + msgid "ProjectActivityRSS|Subscribe" msgstr "" @@ -5934,6 +6075,9 @@ msgstr "" msgid "Search for projects, issues, etc." msgstr "" +msgid "Search groups" +msgstr "" + msgid "Search merge requests" msgstr "" @@ -5949,6 +6093,9 @@ msgstr "" msgid "Search project" msgstr "" +msgid "Search projects" +msgstr "" + msgid "Search users" msgstr "" @@ -6009,6 +6156,9 @@ msgstr "" msgid "Select branch/tag" msgstr "" +msgid "Select members to invite" +msgstr "" + msgid "Select project" msgstr "" @@ -6057,13 +6207,31 @@ msgstr "" msgid "Serverless" msgstr "" +msgid "ServerlessDetails|Copy URL to clipboard" +msgstr "" + +msgid "ServerlessDetails|Kubernetes Pods" +msgstr "" + +msgid "ServerlessDetails|Number of Kubernetes pods in use over time based on necessity." +msgstr "" + +msgid "ServerlessDetails|pod in use" +msgstr "" + +msgid "ServerlessDetails|pods in use" +msgstr "" + msgid "Serverless| In order to start using functions as a service, you must first install Knative on your Kubernetes cluster." msgstr "" msgid "Serverless|An error occurred while retrieving serverless components" msgstr "" -msgid "Serverless|Domain" +msgid "Serverless|Cluster Env" +msgstr "" + +msgid "Serverless|Description" msgstr "" msgid "Serverless|Function" @@ -6224,6 +6392,9 @@ msgstr "" msgid "Slower but makes sure the project workspace is pristine as it clones the repository from scratch for every job" msgstr "" +msgid "Snippet Contents" +msgstr "" + msgid "Snippets" msgstr "" @@ -6761,12 +6932,24 @@ msgstr "" msgid "There are no archived projects yet" msgstr "" +msgid "There are no closed issues" +msgstr "" + +msgid "There are no closed merge requests" +msgstr "" + msgid "There are no issues to show" msgstr "" msgid "There are no labels yet" msgstr "" +msgid "There are no open issues" +msgstr "" + +msgid "There are no open merge requests" +msgstr "" + msgid "There are no projects shared with this group yet" msgstr "" @@ -7158,6 +7341,9 @@ msgstr "" msgid "Title" msgstr "" +msgid "Titles and Filenames" +msgstr "" + msgid "To GitLab" msgstr "" @@ -7191,13 +7377,19 @@ msgstr "" msgid "To import an SVN repository, check out %{svn_link}." msgstr "" +msgid "To keep this project going, create a new issue" +msgstr "" + +msgid "To keep this project going, create a new merge request" +msgstr "" + msgid "To link Sentry to GitLab, enter your Sentry URL and Auth Token." msgstr "" msgid "To move or copy an entire GitLab project from another GitLab installation to this one, navigate to the original project's settings page, generate an export file, and upload it here." msgstr "" -msgid "To preserve performance only <strong>%{display_size} of ${real_size}</strong> files are displayed." +msgid "To preserve performance only <strong>%{display_size} of %{real_size}</strong> files are displayed." msgstr "" msgid "To start serving your jobs you can add Runners to your group" @@ -7473,6 +7665,9 @@ msgstr "" msgid "UserProfile|Subscribe" msgstr "" +msgid "UserProfile|This user doesn't have any personal projects" +msgstr "" + msgid "UserProfile|This user has a private profile" msgstr "" @@ -7572,6 +7767,9 @@ msgstr "" msgid "Want to see the data? Please ask an administrator for access." msgstr "" +msgid "We couldn't find any results matching" +msgstr "" + msgid "We detected potential spam in the %{humanized_resource_name}. Please solve the reCAPTCHA to proceed." msgstr "" @@ -7590,6 +7788,9 @@ msgstr "" msgid "Web terminal" msgstr "" +msgid "What's new?" +msgstr "" + msgid "When a runner is locked, it cannot be assigned to other projects" msgstr "" @@ -7794,6 +7995,9 @@ msgstr "" msgid "You can easily install a Runner on a Kubernetes cluster. %{link_to_help_page}" msgstr "" +msgid "You can invite a new member to <strong>%{project_name}</strong> or invite another group." +msgstr "" + msgid "You can move around the graph by using the arrow keys." msgstr "" @@ -7947,6 +8151,9 @@ msgstr "" msgid "Your name" msgstr "" +msgid "Your project limit is %{limit} projects! Please contact your administrator to increase it" +msgstr "" + msgid "Your projects" msgstr "" @@ -7971,6 +8178,9 @@ msgstr "" msgid "command line instructions" msgstr "" +msgid "commented on %{link_to_project}" +msgstr "" + msgid "confidentiality|You are going to turn off the confidentiality. This means <strong>everyone</strong> will be able to see and leave a comment on this issue." msgstr "" @@ -8029,7 +8239,7 @@ msgstr "" msgid "here" msgstr "" -msgid "http://<sentry-host>/api/0/projects/{organization_slug}/{project_slug}/issues/" +msgid "http://<sentry-host>/api/0/projects/{organization_slug}/{project_slug}/" msgstr "" msgid "https://your-bitbucket-server" @@ -8044,7 +8254,10 @@ msgstr "" msgid "importing" msgstr "" -msgid "in" +msgid "in group %{link_to_group}" +msgstr "" + +msgid "in project %{link_to_project}" msgstr "" msgid "issue boards" @@ -8299,6 +8512,9 @@ msgstr "" msgid "personal access token" msgstr "" +msgid "private" +msgstr "" + msgid "project" msgstr "" diff --git a/package.json b/package.json index 5c4abacae99..75df0ec3ff6 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "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 .", "jest": "BABEL_ENV=jest jest", + "jsdoc": "jsdoc -c config/jsdocs.config.js", "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", @@ -19,12 +20,13 @@ "webpack-prod": "NODE_ENV=production webpack --config config/webpack.config.js" }, "dependencies": { - "@babel/core": "^7.1.2", - "@babel/plugin-proposal-class-properties": "^7.1.0", - "@babel/plugin-proposal-json-strings": "^7.0.0", - "@babel/plugin-syntax-dynamic-import": "^7.0.0", - "@babel/plugin-syntax-import-meta": "^7.0.0", - "@babel/preset-env": "^7.1.0", + "@babel/core": "^7.2.2", + "@babel/plugin-proposal-class-properties": "^7.2.3", + "@babel/plugin-proposal-json-strings": "^7.2.0", + "@babel/plugin-proposal-private-methods": "^7.2.3", + "@babel/plugin-syntax-dynamic-import": "^7.2.0", + "@babel/plugin-syntax-import-meta": "^7.2.0", + "@babel/preset-env": "^7.2.3", "@gitlab/csslab": "^1.8.0", "@gitlab/svgs": "^1.47.0", "@gitlab/ui": "^1.20.0", @@ -32,10 +34,10 @@ "apollo-client": "^2.4.5", "autosize": "^4.0.0", "axios": "^0.17.1", - "babel-loader": "^8.0.4", + "babel-loader": "^8.0.5", "bootstrap": "4.1.3", "brace-expansion": "^1.1.8", - "cache-loader": "^1.2.2", + "cache-loader": "^2.0.1", "chart.js": "1.0.2", "classlist-polyfill": "^1.2.0", "clipboard": "^1.7.1", @@ -54,14 +56,14 @@ "d3-time": "^1.0.8", "d3-time-format": "^2.1.1", "dateformat": "^3.0.3", - "deckar01-task_list": "^2.0.0", + "deckar01-task_list": "^2.0.1", "diff": "^3.4.0", "document-register-element": "1.3.0", "dropzone": "^4.2.0", "echarts": "^4.2.0-rc.2", "emoji-unicode-version": "^0.2.1", "exports-loader": "^0.7.0", - "file-loader": "^2.0.0", + "file-loader": "^3.0.1", "formdata-polyfill": "^3.0.11", "fuzzaldrin-plus": "^0.5.0", "glob": "^7.1.2", @@ -85,7 +87,7 @@ "prismjs": "^1.6.0", "raphael": "^2.2.7", "raven-js": "^3.22.1", - "raw-loader": "^0.5.1", + "raw-loader": "^1.0.0", "sanitize-html": "^1.16.1", "select2": "3.5.2-browserify", "sha1": "^1.1.1", @@ -93,26 +95,26 @@ "sortablejs": "^1.7.0", "sql.js": "^0.4.0", "stickyfilljs": "^2.0.5", - "style-loader": "^0.23.0", + "style-loader": "^0.23.1", "svg4everybody": "2.1.9", "three": "^0.84.0", "three-orbit-controls": "^82.1.0", "three-stl-loader": "^1.0.4", "timeago.js": "^3.0.2", "underscore": "^1.9.0", - "url-loader": "^1.1.1", + "url-loader": "^1.1.2", "visibilityjs": "^1.2.4", - "vue": "^2.5.17", + "vue": "^2.5.21", "vue-apollo": "^3.0.0-beta.25", "vue-loader": "^15.4.2", - "vue-resource": "^1.5.0", - "vue-router": "^3.0.1", - "vue-template-compiler": "^2.5.17", + "vue-resource": "^1.5.1", + "vue-router": "^3.0.2", + "vue-template-compiler": "^2.5.21", "vue-virtual-scroll-list": "^1.2.5", "vuex": "^3.0.1", - "webpack": "^4.19.1", - "webpack-bundle-analyzer": "^3.0.2", - "webpack-cli": "^3.1.0", + "webpack": "^4.28.1", + "webpack-bundle-analyzer": "^3.0.3", + "webpack-cli": "^3.2.1", "webpack-stats-plugin": "^0.2.1", "worker-loader": "^2.0.0", "xterm": "^3.5.0" @@ -131,6 +133,7 @@ "babel-types": "^6.26.0", "chalk": "^2.4.1", "commander": "^2.18.0", + "docdash": "^1.0.2", "eslint": "~5.9.0", "eslint-import-resolver-jest": "^2.1.1", "eslint-import-resolver-webpack": "^0.10.1", @@ -147,6 +150,8 @@ "jasmine-jquery": "^2.1.1", "jest": "^23.6.0", "jest-junit": "^5.2.0", + "jsdoc": "^3.5.5", + "jsdoc-vue": "^1.0.0", "karma": "^3.0.0", "karma-chrome-launcher": "^2.2.0", "karma-coverage-istanbul-reporter": "^2.0.4", @@ -156,12 +161,13 @@ "karma-sourcemap-loader": "^0.3.7", "karma-webpack": "^4.0.0-beta.0", "nodemon": "^1.18.4", - "prettier": "1.15.2", - "vue-jest": "^3.0.1", - "webpack-dev-server": "^3.1.10", + "prettier": "1.15.3", + "vue-jest": "^3.0.2", + "webpack-dev-server": "^3.1.14", "yarn-deduplicate": "^1.0.5" }, "engines": { + "node": ">=8.10.0", "yarn": "^1.10.0" } } diff --git a/qa/README.md b/qa/README.md index 08ba59e117d..5e32496ea9f 100644 --- a/qa/README.md +++ b/qa/README.md @@ -86,7 +86,7 @@ The environment variable `QA_COOKIES` can be set to send additional cookies on every request. This is necessary on gitlab.com to direct traffic to the canary fleet. To do this set `QA_COOKIES="gitlab_canary=true"`. -To set multiple cookies, separate them with the `;` character, for example: `QA_COOKIES="cookie1=value;cookie2=value2"` +To set multiple cookies, separate them with the `;` character, for example: `QA_COOKIES="cookie1=value;cookie2=value2"` ### Building a Docker image to test @@ -100,3 +100,24 @@ docker build -t gitlab/gitlab-ce-qa:nightly . ``` [GDK]: https://gitlab.com/gitlab-org/gitlab-development-kit/ + +### Quarantined tests + +Tests can be put in quarantine by assigning `:quarantine` metadata. This means +they will be skipped unless run with `--tag quarantine`. This can be used for +tests that are expected to fail while a fix is in progress (similar to how +[`skip` or `pending`](https://relishapp.com/rspec/rspec-core/v/3-8/docs/pending-and-skipped-examples) + can be used). + +``` +bin/qa Test::Instance::All http://localhost --tag quarantine +``` + +If `quarantine` is used with other tags, tests will only be run if they have at +least one of the tags other than `quarantine`. This is different from how RSpec +tags usually work, where all tags are inclusive. + +For example, suppose one test has `:smoke` and `:quarantine` metadata, and +another test has `:ldap` and `:quarantine` metadata. If the tests are run with +`--tag smoke --tag quarantine`, only the first test will run. The test with +`:ldap` will not run even though it also has `:quarantine`. diff --git a/qa/Rakefile b/qa/Rakefile new file mode 100644 index 00000000000..8df1cfdc174 --- /dev/null +++ b/qa/Rakefile @@ -0,0 +1,6 @@ +require_relative 'qa/tools/revoke_all_personal_access_tokens' + +desc "Revokes all personal access tokens" +task :revoke_personal_access_tokens do + QA::Tools::RevokeAllPersonalAccessTokens.new.run +end @@ -158,6 +158,10 @@ module QA autoload :Activity, 'qa/page/project/activity' autoload :Menu, 'qa/page/project/menu' + module Branches + autoload :Show, 'qa/page/project/branches/show' + end + module Commit autoload :Show, 'qa/page/project/commit/show' end @@ -191,6 +195,11 @@ module QA autoload :MirroringRepositories, 'qa/page/project/settings/mirroring_repositories' end + module SubMenus + autoload :Common, 'qa/page/project/sub_menus/common' + autoload :Repository, 'qa/page/project/sub_menus/repository' + end + module Issue autoload :New, 'qa/page/project/issue/new' autoload :Show, 'qa/page/project/issue/show' diff --git a/qa/qa/git/repository.rb b/qa/qa/git/repository.rb index 7f959441dac..86e00cdbb9c 100644 --- a/qa/qa/git/repository.rb +++ b/qa/qa/git/repository.rb @@ -46,12 +46,9 @@ module QA run("git clone #{opts} #{uri} ./") end - def checkout(branch_name) - run(%Q{git checkout "#{branch_name}"}) - end - - def checkout_new_branch(branch_name) - run(%Q{git checkout -b "#{branch_name}"}) + def checkout(branch_name, new_branch: false) + opts = new_branch ? '-b' : '' + run(%Q{git checkout #{opts} "#{branch_name}"}).to_s end def shallow_clone @@ -84,6 +81,10 @@ module QA run("git push #{uri} #{branch}") end + def merge(branch) + run("git merge #{branch}") + end + def commits run('git log --oneline').split("\n") end diff --git a/qa/qa/page/component/note.rb b/qa/qa/page/component/note.rb index 67d7f114786..f5add6bc9b5 100644 --- a/qa/qa/page/component/note.rb +++ b/qa/qa/page/component/note.rb @@ -32,9 +32,13 @@ module QA click_element :comment_button end - def reply_to_discussion(reply_text) + def type_reply_to_discussion(reply_text) all_elements(:discussion_reply).last.click fill_element :reply_input, reply_text + end + + def reply_to_discussion(reply_text) + type_reply_to_discussion(reply_text) click_element :reply_comment_button end diff --git a/qa/qa/page/profile/personal_access_tokens.rb b/qa/qa/page/profile/personal_access_tokens.rb index 9191dbe9cf3..8c12eff5cf1 100644 --- a/qa/qa/page/profile/personal_access_tokens.rb +++ b/qa/qa/page/profile/personal_access_tokens.rb @@ -3,29 +3,51 @@ module QA module Profile class PersonalAccessTokens < Page::Base view 'app/views/shared/_personal_access_tokens_form.html.haml' do - element :personal_access_token_name_field, 'text_field :name' # rubocop:disable QA/ElementWithPattern - element :create_token_button, 'submit "Create #{type} token"' # rubocop:disable QA/ElementWithPattern, Lint/InterpolationCheck - element :scopes_api_radios, "label :scopes" # rubocop:disable QA/ElementWithPattern + element :personal_access_token_name_field + element :create_token_button + end + + view 'app/views/shared/tokens/_scopes_form.html.haml' do + element :api_radio, 'qa-#{scope}-radio' # rubocop:disable QA/ElementWithPattern, Lint/InterpolationCheck end view 'app/views/shared/_personal_access_tokens_created_container.html.haml' do - element :create_token_field, "text_field_tag 'created-personal-access-token'" # rubocop:disable QA/ElementWithPattern + element :created_personal_access_token + end + view 'app/views/shared/_personal_access_tokens_table.html.haml' do + element :revoke_button end def fill_token_name(name) - fill_in 'personal_access_token_name', with: name + fill_element(:personal_access_token_name_field, name) end def check_api - check 'personal_access_token_scopes_api' + check_element(:api_radio) end def create_token - click_on 'Create personal access token' + click_element(:create_token_button) end def created_access_token - page.find('#created-personal-access-token').value + find_element(:created_personal_access_token, wait: 30).value + end + + def has_token_row_for_name?(token_name) + page.has_css?('tr', text: token_name, wait: 1.0) + end + + def first_token_row_for_name(token_name) + page.find('tr', text: token_name, match: :first, wait: 1.0) + end + + def revoke_first_token_with_name(token_name) + within first_token_row_for_name(token_name) do + accept_confirm do + click_element(:revoke_button) + end + end end end end diff --git a/qa/qa/page/project/branches/show.rb b/qa/qa/page/project/branches/show.rb new file mode 100644 index 00000000000..762a97e2088 --- /dev/null +++ b/qa/qa/page/project/branches/show.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true + +module QA + module Page + module Project + module Branches + class Show < Page::Base + view 'app/views/projects/branches/_branch.html.haml' do + element :remove_btn + end + view 'app/views/projects/branches/_panel.html.haml' do + element :all_branches + end + view 'app/views/projects/branches/index.html.haml' do + element :delete_merged_branches + end + + def delete_branch(branch_name) + within_element(:all_branches) do + within(".js-branch-#{branch_name}") do + accept_alert do + find_element(:remove_btn).click + end + end + end + end + + def has_branch_title?(branch_title) + within_element(:all_branches) do + within(".item-title") do + has_text?(branch_title) + end + end + end + + def has_branch_with_badge?(branch_name, badge) + within_element(:all_branches) do + within(".js-branch-#{branch_name} .badge") do + has_text?(badge) + end + end + end + + def delete_merged_branches + accept_alert do + 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 + end +end diff --git a/qa/qa/page/project/menu.rb b/qa/qa/page/project/menu.rb index 835e1ed00b5..eb3426b1ac0 100644 --- a/qa/qa/page/project/menu.rb +++ b/qa/qa/page/project/menu.rb @@ -4,11 +4,14 @@ module QA module Page module Project class Menu < Page::Base + include SubMenus::Common + include SubMenus::Repository + view 'app/views/layouts/nav/sidebar/_project.html.haml' do element :settings_item element :settings_link, 'link_to edit_project_path' # rubocop:disable QA/ElementWithPattern - element :repository_link, "title: _('Repository')" # rubocop:disable QA/ElementWithPattern element :link_pipelines + element :link_operations element :link_members_settings element :pipelines_settings_link, "title: _('CI / CD')" # rubocop:disable QA/ElementWithPattern element :operations_kubernetes_link, "title: _('Kubernetes')" # rubocop:disable QA/ElementWithPattern @@ -18,7 +21,6 @@ module QA element :merge_requests_link, /link_to.*shortcuts-merge_requests/ # rubocop:disable QA/ElementWithPattern element :merge_requests_link_text, "Merge Requests" # rubocop:disable QA/ElementWithPattern element :top_level_items, '.sidebar-top-level-items' # rubocop:disable QA/ElementWithPattern - element :operations_section, "class: 'shortcuts-operations'" # rubocop:disable QA/ElementWithPattern element :activity_link, "title: _('Activity')" # rubocop:disable QA/ElementWithPattern element :wiki_link_text, "Wiki" # rubocop:disable QA/ElementWithPattern element :milestones_link @@ -85,12 +87,6 @@ module QA end end - def click_repository - within_sidebar do - click_link('Repository') - end - end - def click_repository_settings hover_settings do within_submenu do @@ -129,6 +125,7 @@ module QA def hover_issues within_sidebar do + scroll_to_element(:issues_item) find_element(:issues_item).hover yield @@ -137,7 +134,8 @@ module QA def hover_operations within_sidebar do - find('.shortcuts-operations').hover + scroll_to_element(:link_operations) + find_element(:link_operations).hover yield end @@ -145,20 +143,9 @@ module QA def hover_settings within_sidebar do - find('.qa-settings-item').hover - - yield - end - end - - def within_sidebar - page.within('.sidebar-top-level-items') do - yield - end - end + scroll_to_element(:settings_item) + find_element(:settings_item).hover - def within_submenu - page.within('.fly-out-list') do yield end end diff --git a/qa/qa/page/project/settings/deploy_keys.rb b/qa/qa/page/project/settings/deploy_keys.rb index 3c8c0cbdf7c..5da8d352e74 100644 --- a/qa/qa/page/project/settings/deploy_keys.rb +++ b/qa/qa/page/project/settings/deploy_keys.rb @@ -14,8 +14,13 @@ module QA end view 'app/assets/javascripts/deploy_keys/components/key.vue' do - element :key_title, /class=".*qa-key-title.*"/ # rubocop:disable QA/ElementWithPattern - element :key_fingerprint, /class=".*qa-key-fingerprint.*"/ # rubocop:disable QA/ElementWithPattern + element :key + element :key_title + element :key_fingerprint + end + + def add_key + click_on 'Add key' end def fill_key_title(title) @@ -26,31 +31,29 @@ module QA fill_in 'deploy_key_key', with: key end - def add_key - click_on 'Add key' - end - - def key_title + def find_fingerprint(title) within_project_deploy_keys do - find_element(:key_title).text + find_element(:key, title) + .find(element_selector_css(:key_fingerprint)).text end end - def key_fingerprint + def has_key?(title, fingerprint) within_project_deploy_keys do - find_element(:key_fingerprint).text + find_element(:key, title) + .has_css?(element_selector_css(:key_fingerprint), text: fingerprint) end end - def key_titles + def key_title within_project_deploy_keys do - all_elements(:key_title) + find_element(:key_title).text end end - def key_fingerprints + def key_fingerprint within_project_deploy_keys do - all_elements(:key_fingerprint) + find_element(:key_fingerprint).text end end @@ -58,7 +61,7 @@ module QA def within_project_deploy_keys wait(reload: false) do - has_css?(element_selector_css(:project_deploy_keys)) + has_element?(:project_deploy_keys) end within_element(:project_deploy_keys) do diff --git a/qa/qa/page/project/sub_menus/common.rb b/qa/qa/page/project/sub_menus/common.rb new file mode 100644 index 00000000000..c94e1e85256 --- /dev/null +++ b/qa/qa/page/project/sub_menus/common.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module QA + module Page + module Project + module SubMenus + module Common + def within_sidebar + within('.sidebar-top-level-items') do + yield + end + end + + def within_submenu + within('.fly-out-list') do + yield + end + end + end + end + end + end +end diff --git a/qa/qa/page/project/sub_menus/repository.rb b/qa/qa/page/project/sub_menus/repository.rb new file mode 100644 index 00000000000..29eaa9a74de --- /dev/null +++ b/qa/qa/page/project/sub_menus/repository.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +module QA + module Page + module Project + module SubMenus + module Repository + def self.included(base) + base.class_eval do + view 'app/views/layouts/nav/sidebar/_project.html.haml' do + element :project_menu_repo + element :branches_link + end + end + end + + def click_repository + within_sidebar do + click_element(:project_menu_repo) + end + end + + def click_repository_branches + hover_repository do + within_submenu do + click_element(:branches_link) + end + end + end + + private + + def hover_repository + within_sidebar do + find_element(:project_menu_repo).hover + + yield + end + end + end + end + end + end +end diff --git a/qa/qa/resource/base.rb b/qa/qa/resource/base.rb index dcea144ab74..f325162d1c0 100644 --- a/qa/qa/resource/base.rb +++ b/qa/qa/resource/base.rb @@ -126,10 +126,6 @@ module QA mod end - def self.attributes_names - dynamic_attributes.instance_methods(false).sort.grep_v(/=$/) - end - class DSL def initialize(base) @base = base diff --git a/qa/qa/resource/deploy_key.rb b/qa/qa/resource/deploy_key.rb index 9ed8fb7726e..9565598efb0 100644 --- a/qa/qa/resource/deploy_key.rb +++ b/qa/qa/resource/deploy_key.rb @@ -8,11 +8,7 @@ module QA attribute :fingerprint do Page::Project::Settings::Repository.perform do |setting| setting.expand_deploy_keys do |key| - key_offset = key.key_titles.index do |key_title| - key_title.text == title - end - - key.key_fingerprints[key_offset].text + key.find_fingerprint(title) end end end diff --git a/qa/qa/resource/repository/push.rb b/qa/qa/resource/repository/push.rb index c14d97ff7fb..f33aa0acef0 100644 --- a/qa/qa/resource/repository/push.rb +++ b/qa/qa/resource/repository/push.rb @@ -64,7 +64,7 @@ module QA repository.configure_identity(username, email) if new_branch - repository.checkout_new_branch(branch_name) + repository.checkout(branch_name, new_branch: true) else repository.checkout(branch_name) end diff --git a/qa/qa/resource/user.rb b/qa/qa/resource/user.rb index c26f0c84a1f..b9580d81171 100644 --- a/qa/qa/resource/user.rb +++ b/qa/qa/resource/user.rb @@ -6,9 +6,12 @@ module QA module Resource class User < Base attr_reader :unique_id - attr_writer :username, :password, :name, :email + attr_writer :username, :password attr_accessor :provider, :extern_uid + attribute :name + attribute :email + def initialize @unique_id = SecureRandom.hex(8) end @@ -22,11 +25,11 @@ module QA end def name - @name ||= username + @name ||= api_resource&.dig(:name) || username end def email - @email ||= "#{username}@example.com" + @email ||= api_resource&.dig(:email) || "#{username}@example.com" end def credentials_given? diff --git a/qa/qa/runtime/browser.rb b/qa/qa/runtime/browser.rb index b706d6565d2..0bcf5e693f0 100644 --- a/qa/qa/runtime/browser.rb +++ b/qa/qa/runtime/browser.rb @@ -40,34 +40,38 @@ module QA return if Capybara.drivers.include?(:chrome) - Capybara.register_driver :chrome do |app| - capabilities = Selenium::WebDriver::Remote::Capabilities.chrome( - # This enables access to logs with `page.driver.manage.get_log(:browser)` - loggingPrefs: { - browser: "ALL", - client: "ALL", - driver: "ALL", - server: "ALL" - } - ) + Capybara.register_driver QA::Runtime::Env.browser do |app| + capabilities = Selenium::WebDriver::Remote::Capabilities.send(QA::Runtime::Env.browser, + # This enables access to logs with `page.driver.manage.get_log(:browser)` + loggingPrefs: { + browser: "ALL", + client: "ALL", + driver: "ALL", + server: "ALL" + }) if QA::Runtime::Env.accept_insecure_certs? capabilities['acceptInsecureCerts'] = true end - options = Selenium::WebDriver::Chrome::Options.new - options.add_argument("window-size=1240,1680") + # QA::Runtime::Env.browser.capitalize will work for every driver type except PhantomJS. + # We will have no use to use PhantomJS so this shouldn't be a problem. + options = Selenium::WebDriver.const_get(QA::Runtime::Env.browser.capitalize)::Options.new + + if QA::Runtime::Env.browser == :chrome + options.add_argument("window-size=1240,1680") - # Chrome won't work properly in a Docker container in sandbox mode - options.add_argument("no-sandbox") + # Chrome won't work properly in a Docker container in sandbox mode + options.add_argument("no-sandbox") - # Run headless by default unless CHROME_HEADLESS is false - if QA::Runtime::Env.chrome_headless? - options.add_argument("headless") + # Run headless by default unless CHROME_HEADLESS is false + if QA::Runtime::Env.chrome_headless? + options.add_argument("headless") - # Chrome documentation says this flag is needed for now - # https://developers.google.com/web/updates/2017/04/headless-chrome#cli - options.add_argument("disable-gpu") + # Chrome documentation says this flag is needed for now + # https://developers.google.com/web/updates/2017/04/headless-chrome#cli + options.add_argument("disable-gpu") + end end # Use the same profile on QA runs if CHROME_REUSE_PROFILE is true. @@ -80,12 +84,18 @@ module QA # Disable /dev/shm use in CI. See https://gitlab.com/gitlab-org/gitlab-ee/issues/4252 options.add_argument("disable-dev-shm-usage") if QA::Runtime::Env.running_in_ci? - Capybara::Selenium::Driver.new( - app, - browser: :chrome, + selenium_options = { + browser: QA::Runtime::Env.browser, clear_local_storage: true, desired_capabilities: capabilities, options: options + } + + selenium_options[:url] = QA::Runtime::Env.remote_grid if QA::Runtime::Env.remote_grid + + Capybara::Selenium::Driver.new( + app, + selenium_options ) end @@ -93,7 +103,7 @@ module QA Capybara::Screenshot.prune_strategy = :keep_last_run # From https://github.com/mattheworiordan/capybara-screenshot/issues/84#issuecomment-41219326 - Capybara::Screenshot.register_driver(:chrome) do |driver, path| + Capybara::Screenshot.register_driver(QA::Runtime::Env.browser) do |driver, path| driver.browser.save_screenshot(path) end @@ -102,8 +112,8 @@ module QA end Capybara.configure do |config| - config.default_driver = :chrome - config.javascript_driver = :chrome + config.default_driver = QA::Runtime::Env.browser + config.javascript_driver = QA::Runtime::Env.browser config.default_max_wait_time = 10 # https://github.com/mattheworiordan/capybara-screenshot/issues/164 config.save_path = ::File.expand_path('../../tmp', __dir__) diff --git a/qa/qa/runtime/env.rb b/qa/qa/runtime/env.rb index dae5aa3f794..79b40223d84 100644 --- a/qa/qa/runtime/env.rb +++ b/qa/qa/runtime/env.rb @@ -56,6 +56,34 @@ module QA @personal_access_token ||= ENV['PERSONAL_ACCESS_TOKEN'] end + def remote_grid + # if username specified, password/auth token is required + # can be + # - "http://user:pass@somehost.com/wd/hub" + # - "https://user:pass@somehost.com:443/wd/hub" + # - "http://localhost:4444/wd/hub" + + return unless ENV['QA_REMOTE_GRID'] + + "#{remote_grid_protocol}://#{remote_grid_credentials}#{ENV['QA_REMOTE_GRID']}/wd/hub" + end + + def remote_grid_username + ENV['QA_REMOTE_GRID_USERNAME'] + end + + def remote_grid_access_key + ENV['QA_REMOTE_GRID_ACCESS_KEY'] + end + + def remote_grid_protocol + ENV['QA_REMOTE_GRID_PROTOCOL'] || 'http' + end + + def browser + ENV['QA_BROWSER'].nil? ? :chrome : ENV['QA_BROWSER'].to_sym + end + def user_username ENV['GITLAB_USERNAME'] end @@ -158,6 +186,16 @@ module QA private + def remote_grid_credentials + if remote_grid_username + raise ArgumentError, %Q(Please provide an access key for user "#{remote_grid_username}") unless remote_grid_access_key + + return "#{remote_grid_username}:#{remote_grid_access_key}@" + end + + '' + end + def enabled?(value, default: true) return default if value.nil? 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 new file mode 100644 index 00000000000..0f0c627d79a --- /dev/null +++ b/qa/qa/specs/features/browser_ui/3_create/repository/add_list_delete_branches_spec.rb @@ -0,0 +1,92 @@ +# frozen_string_literal: true + +module QA + context 'Create' do + describe 'Create, list, and delete branches via web' do + master_branch = 'master' + second_branch = 'second-branch' + third_branch = 'third-branch' + file_1_master = 'file.txt' + file_2_master = 'other-file.txt' + file_second_branch = 'file-2.txt' + file_third_branch = 'file-3.txt' + first_commit_message_of_master_branch = "Add #{file_1_master}" + second_commit_message_of_master_branch = "Add #{file_2_master}" + commit_message_of_second_branch = "Add #{file_second_branch}" + commit_message_of_third_branch = "Add #{file_third_branch}" + + before do + Runtime::Browser.visit(:gitlab, Page::Main::Login) + Page::Main::Login.perform(&:sign_in_using_credentials) + + project = Resource::Project.fabricate! do |proj| + proj.name = 'project-qa-test' + proj.description = 'project for qa test' + end + project.visit! + + Git::Repository.perform do |repository| + repository.uri = project.repository_http_location.uri + repository.use_default_credentials + + repository.act do + clone + configure_identity('GitLab QA', 'root@gitlab.com') + commit_file(file_1_master, 'Test file content', first_commit_message_of_master_branch) + push_changes + checkout(second_branch, new_branch: true) + commit_file(file_second_branch, 'File 2 content', commit_message_of_second_branch) + push_changes(second_branch) + checkout(master_branch) + # This second commit on master is needed for the master branch to be ahead + # of the second branch, and when the second branch is merged to master it will + # show the 'merged' badge on it. + # Refer to the below issue note: + # https://gitlab.com/gitlab-org/gitlab-ce/issues/55524#note_126100848 + commit_file(file_2_master, 'Other test file content', second_commit_message_of_master_branch) + push_changes + merge(second_branch) + push_changes + checkout(third_branch, new_branch: true) + commit_file(file_third_branch, 'File 3 content', commit_message_of_third_branch) + push_changes(third_branch) + end + end + Page::Project::Show.perform(&:wait_for_push) + end + + it 'branches are correctly listed after CRUD operations' do + Page::Project::Menu.perform(&:click_repository_branches) + + expect(page).to have_content(master_branch) + expect(page).to have_content(second_branch) + expect(page).to have_content(third_branch) + expect(page).to have_content("Merge branch 'second-branch'") + expect(page).to have_content(commit_message_of_second_branch) + expect(page).to have_content(commit_message_of_third_branch) + + Page::Project::Branches::Show.perform do |branches| + expect(branches).to have_branch_with_badge(second_branch, 'merged') + end + + Page::Project::Branches::Show.perform do |branches_view| + branches_view.delete_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( + 'Merged branches are being deleted. This can take some time depending on the number of branches. Please refresh the page to see changes.' + ) + + 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) + end + end + end + end +end diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/user_views_raw_diff_patch_requests_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/user_views_raw_diff_patch_requests_spec.rb index 203338ddf77..3a5d89e6b83 100644 --- a/qa/qa/specs/features/browser_ui/3_create/repository/user_views_raw_diff_patch_requests_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/repository/user_views_raw_diff_patch_requests_spec.rb @@ -39,11 +39,15 @@ module QA end it 'user views raw email patch' do + user = Resource::User.fabricate_via_api! do |user| + user.username = Runtime::User.username + end + view_commit Page::Project::Commit::Show.perform(&:select_email_patches) - expect(page).to have_content('From: Administrator <admin@example.com>') + expect(page).to have_content("From: #{user.name} <#{user.email}>") expect(page).to have_content('Subject: [PATCH] Add second file') expect(page).to have_content('diff --git a/second b/second') end diff --git a/qa/qa/specs/features/browser_ui/3_create/wiki/create_edit_clone_push_wiki_spec.rb b/qa/qa/specs/features/browser_ui/3_create/wiki/create_edit_clone_push_wiki_spec.rb index 210271705d9..a7d0998d42c 100644 --- a/qa/qa/specs/features/browser_ui/3_create/wiki/create_edit_clone_push_wiki_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/wiki/create_edit_clone_push_wiki_spec.rb @@ -17,7 +17,8 @@ module QA login end - it 'user creates, edits, clones, and pushes to the wiki' do + # Failure reported: https://gitlab.com/gitlab-org/quality/nightly/issues/24 + it 'user creates, edits, clones, and pushes to the wiki', :quarantine do wiki = Resource::Wiki.fabricate! do |resource| resource.title = 'Home' resource.content = '# My First Wiki Content' diff --git a/qa/qa/specs/features/browser_ui/6_release/deploy_key/add_deploy_key_spec.rb b/qa/qa/specs/features/browser_ui/6_release/deploy_key/add_deploy_key_spec.rb index 84757f25379..6f39a755392 100644 --- a/qa/qa/specs/features/browser_ui/6_release/deploy_key/add_deploy_key_spec.rb +++ b/qa/qa/specs/features/browser_ui/6_release/deploy_key/add_deploy_key_spec.rb @@ -5,7 +5,7 @@ module QA describe 'Deploy key creation' do it 'user adds a deploy key' do Runtime::Browser.visit(:gitlab, Page::Main::Login) - Page::Main::Login.act { sign_in_using_credentials } + Page::Main::Login.perform(&:sign_in_using_credentials) key = Runtime::Key::RSA.new deploy_key_title = 'deploy key title' @@ -16,7 +16,13 @@ module QA resource.key = deploy_key_value end - expect(deploy_key.fingerprint).to eq(key.fingerprint) + expect(deploy_key.fingerprint).to eq key.fingerprint + + Page::Project::Settings::Repository.perform do |setting| + setting.expand_deploy_keys do |keys| + expect(keys).to have_key(deploy_key_title, key.fingerprint) + end + end end end end diff --git a/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb b/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb index 5ee8df03d50..5147b17d7ab 100644 --- a/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb +++ b/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb @@ -98,6 +98,17 @@ module QA resource.value = 'You can see this application secret' end + # Our current Auto DevOps implementation won't update the production + # app if we only update a CI variable with no code change. + # + # Workaround: push new code and use the resultant pipeline. + Resource::Repository::ProjectPush.fabricate! do |push| + push.project = @project + push.commit_message = 'Force a Deployment change by pushing new code' + push.file_name = 'new_file.txt' + push.file_content = 'new file contents' + end + @project.visit! Page::Project::Menu.act { click_ci_cd_pipelines } Page::Project::Pipeline::Index.act { go_to_latest_pipeline } diff --git a/qa/qa/tools/revoke_all_personal_access_tokens.rb b/qa/qa/tools/revoke_all_personal_access_tokens.rb new file mode 100644 index 00000000000..7484b633bf6 --- /dev/null +++ b/qa/qa/tools/revoke_all_personal_access_tokens.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +require_relative '../../qa' +require 'net/protocol.rb' +# This script revokes all personal access tokens with the name of 'api-test-token' on the host specified by GITLAB_ADDRESS +# Required environment variables: GITLAB_USERNAME, GITLAB_PASSWORD and GITLAB_ADDRESS +# Run `rake revoke_personal_access_tokens` + +module QA + module Tools + class RevokeAllPersonalAccessTokens + def run + do_run + rescue Net::ReadTimeout + STDOUT.puts 'Net::ReadTimeout during run. Trying again' + run + end + + private + + def do_run + raise ArgumentError, "Please provide GITLAB_USERNAME" unless ENV['GITLAB_USERNAME'] + raise ArgumentError, "Please provide GITLAB_PASSWORD" unless ENV['GITLAB_PASSWORD'] + raise ArgumentError, "Please provide GITLAB_ADDRESS" unless ENV['GITLAB_ADDRESS'] + + STDOUT.puts 'Running...' + + Runtime::Browser.visit(ENV['GITLAB_ADDRESS'], Page::Main::Login) + Page::Main::Login.perform(&:sign_in_using_credentials) + Page::Main::Menu.perform(&:go_to_profile_settings) + Page::Profile::Menu.perform(&:click_access_tokens) + + token_name = 'api-test-token' + + Page::Profile::PersonalAccessTokens.perform do |page| + while page.has_token_row_for_name?(token_name) + page.revoke_first_token_with_name(token_name) + print "\e[32m.\e[0m" + end + end + end + end + end +end diff --git a/qa/spec/resource/base_spec.rb b/qa/spec/resource/base_spec.rb index dc9e16792d3..b8c406ae72a 100644 --- a/qa/spec/resource/base_spec.rb +++ b/qa/spec/resource/base_spec.rb @@ -138,10 +138,6 @@ describe QA::Resource::Base do describe '.attribute' do include_context 'simple resource' - it 'appends new attribute' do - expect(subject.attributes_names).to eq([:no_block, :test, :web_url]) - end - context 'when the attribute is populated via a block' do it 'returns value from the block' do result = subject.fabricate!(resource: resource) diff --git a/qa/spec/runtime/env_spec.rb b/qa/spec/runtime/env_spec.rb index ded51d5bb7c..bc0ec08d66d 100644 --- a/qa/spec/runtime/env_spec.rb +++ b/qa/spec/runtime/env_spec.rb @@ -207,4 +207,63 @@ describe QA::Runtime::Env do expect { described_class.can_test? :foo }.to raise_error(ArgumentError, 'Unknown feature "foo"') end end + + describe 'remote grid credentials' do + it 'is blank if username is empty' do + stub_env('QA_REMOTE_GRID_USERNAME', nil) + + expect(described_class.send(:remote_grid_credentials)).to eq('') + end + + it 'throws ArgumentError if GRID_ACCESS_KEY is not specified with USERNAME' do + stub_env('QA_REMOTE_GRID_USERNAME', 'foo') + + expect { described_class.send(:remote_grid_credentials) }.to raise_error(ArgumentError, 'Please provide an access key for user "foo"') + end + + it 'returns a user:key@ combination when all args are satiated' do + stub_env('QA_REMOTE_GRID_USERNAME', 'foo') + stub_env('QA_REMOTE_GRID_ACCESS_KEY', 'bar') + + expect(described_class.send(:remote_grid_credentials)).to eq('foo:bar@') + end + end + + describe '.remote_grid_protocol' do + it 'defaults protocol to http' do + stub_env('QA_REMOTE_GRID_PROTOCOL', nil) + expect(described_class.remote_grid_protocol).to eq('http') + end + end + + describe '.remote_grid' do + it 'is falsey if QA_REMOTE_GRID is not set' do + expect(described_class.remote_grid).to be_falsey + end + + it 'accepts https protocol' do + stub_env('QA_REMOTE_GRID', 'localhost:4444') + stub_env('QA_REMOTE_GRID_PROTOCOL', 'https') + + expect(described_class.remote_grid).to eq('https://localhost:4444/wd/hub') + end + + context 'with credentials' do + it 'has a grid of http://user:key@grid/wd/hub' do + stub_env('QA_REMOTE_GRID_USERNAME', 'foo') + stub_env('QA_REMOTE_GRID_ACCESS_KEY', 'bar') + stub_env('QA_REMOTE_GRID', 'localhost:4444') + + expect(described_class.remote_grid).to eq('http://foo:bar@localhost:4444/wd/hub') + end + end + + context 'without credentials' do + it 'has a grid of http://grid/wd/hub' do + stub_env('QA_REMOTE_GRID', 'localhost:4444') + + expect(described_class.remote_grid).to eq('http://localhost:4444/wd/hub') + end + end + end end diff --git a/qa/spec/spec_helper.rb b/qa/spec/spec_helper.rb index 8e01da01340..3537ba7c235 100644 --- a/qa/spec/spec_helper.rb +++ b/qa/spec/spec_helper.rb @@ -5,6 +5,24 @@ Dir[::File.join(__dir__, 'support', '**', '*.rb')].each { |f| require f } RSpec.configure do |config| config.before do |example| QA::Runtime::Logger.debug("Starting test: #{example.full_description}") if QA::Runtime::Env.debug? + + # If quarantine is tagged, skip tests that have other metadata unless + # they're also tagged. This lets us run quarantined tests in a particular + # category without running tests in other categories. + # E.g., if a test is tagged 'smoke' and 'quarantine', and another is tagged + # 'ldap' and 'quarantine', if we wanted to run just quarantined smoke tests + # using `--tag quarantine --tag smoke`, without this check we'd end up + # running that ldap test as well. + if config.inclusion_filter[:quarantine] + skip("Running tests tagged with all of #{config.inclusion_filter.rules.keys}") unless quarantine_and_optional_other_tag?(example, config) + end + end + + config.before(:each, :quarantine) do |example| + # Skip tests in quarantine unless we explicitly focus on them + # We could use an exclusion filter, but this way the test report will list + # the quarantined tests when they're not run so that we're aware of them + skip('In quarantine') unless config.inclusion_filter[:quarantine] end config.expect_with :rspec do |expectations| @@ -22,3 +40,19 @@ RSpec.configure do |config| config.order = :random Kernel.srand config.seed end + +# Checks if a test has the 'quarantine' tag and other tags in the inclusion filter. +# +# Returns true if +# - the example metadata includes the quarantine tag +# - and the metadata and inclusion filter both have any other tag +# - or no other tags are in the inclusion filter +def quarantine_and_optional_other_tag?(example, config) + return false unless example.metadata.keys.include? :quarantine + + filters_other_than_quarantine = config.inclusion_filter.rules.keys.reject { |key| key == :quarantine } + + return true if filters_other_than_quarantine.empty? + + filters_other_than_quarantine.any? { |key| example.metadata.keys.include? key } +end diff --git a/qa/spec/spec_helper_spec.rb b/qa/spec/spec_helper_spec.rb new file mode 100644 index 00000000000..f001200fb52 --- /dev/null +++ b/qa/spec/spec_helper_spec.rb @@ -0,0 +1,264 @@ +# frozen_string_literal: true + +describe 'rspec config tests' do + let(:group) do + RSpec.describe do + shared_examples 'passing tests' do + example 'not in quarantine' do + end + example 'in quarantine', :quarantine do + end + end + + context 'foo', :foo do + it_behaves_like 'passing tests' + end + + context 'default' do + it_behaves_like 'passing tests' + end + end + end + + context 'default config' do + it 'tests are skipped if in quarantine' do + group.run + + foo_context = group.children.find { |c| c.description == "foo" } + foo_examples = foo_context.descendant_filtered_examples + expect(foo_examples.count).to eq(2) + + ex = foo_examples.find { |e| e.description == "not in quarantine" } + expect(ex.execution_result.status).to eq(:passed) + + ex = foo_examples.find { |e| e.description == "in quarantine" } + expect(ex.execution_result.status).to eq(:pending) + expect(ex.execution_result.pending_message).to eq('In quarantine') + + default_context = group.children.find { |c| c.description == "default" } + default_examples = default_context.descendant_filtered_examples + expect(default_examples.count).to eq(2) + + ex = default_examples.find { |e| e.description == "not in quarantine" } + expect(ex.execution_result.status).to eq(:passed) + + ex = default_examples.find { |e| e.description == "in quarantine" } + expect(ex.execution_result.status).to eq(:pending) + expect(ex.execution_result.pending_message).to eq('In quarantine') + end + end + + context "with 'quarantine' tagged" do + before do + RSpec.configure do |config| + config.inclusion_filter = :quarantine + end + end + after do + RSpec.configure do |config| + config.inclusion_filter.clear + end + end + + it "only quarantined tests are run" do + group.run + + foo_context = group.children.find { |c| c.description == "foo" } + foo_examples = foo_context.descendant_filtered_examples + expect(foo_examples.count).to be(1) + + ex = foo_examples.find { |e| e.description == "in quarantine" } + expect(ex.execution_result.status).to eq(:passed) + + default_context = group.children.find { |c| c.description == "default" } + default_examples = default_context.descendant_filtered_examples + expect(default_examples.count).to be(1) + + ex = default_examples.find { |e| e.description == "in quarantine" } + expect(ex.execution_result.status).to eq(:passed) + end + end + + context "with 'foo' tagged" do + before do + RSpec.configure do |config| + config.inclusion_filter = :foo + end + + group.run + end + after do + RSpec.configure do |config| + config.inclusion_filter.clear + end + end + + it "tests are not run if not tagged 'foo'" do + default_context = group.children.find { |c| c.description == "default" } + expect(default_context.descendant_filtered_examples.count).to eq(0) + end + + it "tests are skipped if in quarantine" do + foo_context = group.children.find { |c| c.description == "foo" } + foo_examples = foo_context.descendant_filtered_examples + expect(foo_examples.count).to eq(2) + + ex = foo_examples.find { |e| e.description == "not in quarantine" } + expect(ex.execution_result.status).to eq(:passed) + + ex = foo_examples.find { |e| e.description == "in quarantine" } + expect(ex.execution_result.status).to eq(:pending) + expect(ex.execution_result.pending_message).to eq('In quarantine') + end + end + + context "with 'quarantine' and 'foo' tagged" do + before do + RSpec.configure do |config| + config.inclusion_filter = { quarantine: true, foo: true } + end + end + after do + RSpec.configure do |config| + config.inclusion_filter.clear + end + end + + it 'of tests tagged foo, only tests in quarantine run' do + group.run + + foo_context = group.children.find { |c| c.description == "foo" } + foo_examples = foo_context.descendant_filtered_examples + expect(foo_examples.count).to eq(2) + + ex = foo_examples.find { |e| e.description == "not in quarantine" } + expect(ex.execution_result.status).to eq(:pending) + expect(ex.execution_result.pending_message).to eq('Running tests tagged with all of [:quarantine, :foo]') + + ex = foo_examples.find { |e| e.description == "in quarantine" } + expect(ex.execution_result.status).to eq(:passed) + end + + it 'if tests are not tagged they are skipped, even if they are in quarantine' do + group.run + default_context = group.children.find { |c| c.description == "default" } + default_examples = default_context.descendant_filtered_examples + expect(default_examples.count).to eq(1) + + ex = default_examples.find { |e| e.description == "in quarantine" } + expect(ex.execution_result.status).to eq(:pending) + expect(ex.execution_result.pending_message).to eq('Running tests tagged with all of [:quarantine, :foo]') + end + end + + context "with 'foo' and 'bar' tagged" do + before do + RSpec.configure do |config| + config.inclusion_filter = { bar: true, foo: true } + end + end + after do + RSpec.configure do |config| + config.inclusion_filter.clear + end + end + + it "runs tests tagged either 'foo' or 'bar'" do + group = RSpec.describe do + example 'foo', :foo do + end + example 'bar', :bar do + end + example 'foo and bar', :foo, :bar do + end + end + + group.run + expect(group.examples.count).to eq(3) + + ex = group.examples.find { |e| e.description == "foo" } + expect(ex.execution_result.status).to eq(:passed) + + ex = group.examples.find { |e| e.description == "bar" } + expect(ex.execution_result.status).to eq(:passed) + + ex = group.examples.find { |e| e.description == "foo and bar" } + expect(ex.execution_result.status).to eq(:passed) + end + + it "skips quarantined tests tagged 'foo' and/or 'bar'" do + group = RSpec.describe do + example 'foo in quarantine', :foo, :quarantine do + end + example 'foo and bar in quarantine', :foo, :bar, :quarantine do + end + end + + group.run + expect(group.examples.count).to eq(2) + + ex = group.examples.find { |e| e.description == "foo in quarantine" } + expect(ex.execution_result.status).to eq(:pending) + expect(ex.execution_result.pending_message).to eq('In quarantine') + + ex = group.examples.find { |e| e.description == "foo and bar in quarantine" } + expect(ex.execution_result.status).to eq(:pending) + expect(ex.execution_result.pending_message).to eq('In quarantine') + end + + it "ignores quarantined tests not tagged either 'foo' or 'bar'" do + group = RSpec.describe do + example 'in quarantine', :quarantine do + end + end + + group.run + + ex = group.examples.find { |e| e.description == "in quarantine" } + expect(ex.execution_result.status).to be_nil + end + end + + context "with 'foo' and 'bar' and 'quarantined' tagged" do + before do + RSpec.configure do |config| + config.inclusion_filter = { bar: true, foo: true, quarantine: true } + end + end + after do + RSpec.configure do |config| + config.inclusion_filter.clear + end + end + + it "runs tests tagged 'quarantine' and 'foo' or 'bar'" do + group = RSpec.describe do + example 'foo', :foo do + end + example 'bar and quarantine', :bar, :quarantine do + end + example 'foo and bar', :foo, :bar do + end + example 'foo, bar, and quarantine', :foo, :bar, :quarantine do + end + end + + group.run + expect(group.examples.count).to eq(4) + + ex = group.examples.find { |e| e.description == "foo" } + expect(ex.execution_result.status).to eq(:pending) + expect(ex.execution_result.pending_message).to eq('Running tests tagged with all of [:bar, :foo, :quarantine]') + + ex = group.examples.find { |e| e.description == "bar and quarantine" } + expect(ex.execution_result.status).to eq(:passed) + + ex = group.examples.find { |e| e.description == "foo and bar" } + expect(ex.execution_result.status).to eq(:pending) + expect(ex.execution_result.pending_message).to eq('Running tests tagged with all of [:bar, :foo, :quarantine]') + + ex = group.examples.find { |e| e.description == "foo, bar, and quarantine" } + expect(ex.execution_result.status).to eq(:passed) + end + end +end diff --git a/rubocop/cop/inject_enterprise_edition_module.rb b/rubocop/cop/inject_enterprise_edition_module.rb index c8b8aca51ab..1d37b1bd12d 100644 --- a/rubocop/cop/inject_enterprise_edition_module.rb +++ b/rubocop/cop/inject_enterprise_edition_module.rb @@ -11,9 +11,13 @@ module RuboCop METHODS = Set.new(%i[include extend prepend]).freeze - def_node_matcher :ee_const?, <<~PATTERN - (const (const _ :EE) _) - PATTERN + def ee_const?(node) + line = node.location.expression.source_line + + # We use `match?` here instead of RuboCop's AST matching, as this makes + # it far easier to handle nested constants such as `EE::Foo::Bar::Baz`. + line.match?(/(\s|\()(::)?EE::/) + end def on_send(node) return unless METHODS.include?(node.children[1]) diff --git a/rubocop/spec_helpers.rb b/rubocop/spec_helpers.rb index 9bf5f1e3b18..63c1b975a65 100644 --- a/rubocop/spec_helpers.rb +++ b/rubocop/spec_helpers.rb @@ -1,6 +1,7 @@ module RuboCop module SpecHelpers SPEC_HELPERS = %w[fast_spec_helper.rb rails_helper.rb spec_helper.rb].freeze + MIGRATION_SPEC_DIRECTORIES = ['spec/migrations', 'spec/lib/gitlab/background_migration'].freeze # Returns true if the given node originated from the spec directory. def in_spec?(node) @@ -10,14 +11,18 @@ module RuboCop path.start_with?(File.join(Dir.pwd, 'spec'), File.join(Dir.pwd, 'ee', 'spec')) end + def migration_directories + @migration_directories ||= MIGRATION_SPEC_DIRECTORIES.map do |dir| + [File.join(Dir.pwd, dir), File.join(Dir.pwd, 'ee', dir)] + end.flatten + end + # Returns true if the given node originated from a migration spec. def in_migration_spec?(node) path = node.location.expression.source_buffer.name in_spec?(node) && - path.start_with?( - File.join(Dir.pwd, 'spec', 'migrations'), - File.join(Dir.pwd, 'ee', 'spec', 'migrations')) + path.start_with?(*migration_directories) end end end diff --git a/scripts/review_apps/review-apps.sh b/scripts/review_apps/review-apps.sh index 118a7c7f638..4e1dbff7b80 100755 --- a/scripts/review_apps/review-apps.sh +++ b/scripts/review_apps/review-apps.sh @@ -31,7 +31,9 @@ function ensure_namespace() { function install_tiller() { echo "Checking Tiller..." - helm init --upgrade + helm init \ + --upgrade \ + --replicas 2 kubectl rollout status -n "$TILLER_NAMESPACE" -w "deployment/tiller-deploy" if ! helm version --debug; then echo "Failed to init Tiller." diff --git a/spec/controllers/admin/requests_profiles_controller_spec.rb b/spec/controllers/admin/requests_profiles_controller_spec.rb new file mode 100644 index 00000000000..10850cb4603 --- /dev/null +++ b/spec/controllers/admin/requests_profiles_controller_spec.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Admin::RequestsProfilesController do + set(:admin) { create(:admin) } + + before do + sign_in(admin) + end + + describe '#show' do + let(:basename) { "profile_#{Time.now.to_i}.html" } + let(:tmpdir) { Dir.mktmpdir('profiler-test') } + let(:test_file) { File.join(tmpdir, basename) } + let(:profile) { Gitlab::RequestProfiler::Profile.new(basename) } + let(:sample_data) do + <<~HTML + <!DOCTYPE html> + <html> + <body> + <h1>My First Heading</h1> + <p>My first paragraph.</p> + </body> + </html> + HTML + end + + before do + stub_const('Gitlab::RequestProfiler::PROFILES_DIR', tmpdir) + output = File.open(test_file, 'w') + output.write(sample_data) + output.close + end + + after do + File.unlink(test_file) + end + + it 'loads an HTML profile' do + get :show, params: { name: basename } + + expect(response).to have_gitlab_http_status(200) + expect(response.body).to eq(sample_data) + end + end +end diff --git a/spec/controllers/application_controller_spec.rb b/spec/controllers/application_controller_spec.rb index 43f561f7a25..c290acb72aa 100644 --- a/spec/controllers/application_controller_spec.rb +++ b/spec/controllers/application_controller_spec.rb @@ -519,12 +519,14 @@ describe ApplicationController do get :index expect(response).to have_gitlab_http_status(404) + expect(response).to render_template('errors/not_found') end it 'renders a 403 when a message is passed to access denied' do get :index, params: { message: 'None shall pass' } expect(response).to have_gitlab_http_status(403) + expect(response).to render_template('errors/access_denied') end it 'renders a status passed to access denied' do diff --git a/spec/controllers/import/bitbucket_server_controller_spec.rb b/spec/controllers/import/bitbucket_server_controller_spec.rb index 73195463a50..bb282db5a41 100644 --- a/spec/controllers/import/bitbucket_server_controller_spec.rb +++ b/spec/controllers/import/bitbucket_server_controller_spec.rb @@ -78,7 +78,7 @@ describe Import::BitbucketServerController do end it "returns an error when the server can't be contacted" do - expect(client).to receive(:repo).with(project_key, repo_slug).and_raise(BitbucketServer::Client::ServerError) + expect(client).to receive(:repo).with(project_key, repo_slug).and_raise(::BitbucketServer::Connection::ConnectionError) post :create, params: { project: project_key, repository: repo_slug }, format: :json diff --git a/spec/controllers/omniauth_callbacks_controller_spec.rb b/spec/controllers/omniauth_callbacks_controller_spec.rb index d377d69457f..59463462e5a 100644 --- a/spec/controllers/omniauth_callbacks_controller_spec.rb +++ b/spec/controllers/omniauth_callbacks_controller_spec.rb @@ -45,6 +45,40 @@ describe OmniauthCallbacksController, type: :controller do end end + context 'when a redirect fragment is provided' do + let(:provider) { :jwt } + let(:extern_uid) { 'my-uid' } + + before do + request.env['omniauth.params'] = { 'redirect_fragment' => 'L101' } + end + + context 'when a redirect url is stored' do + it 'redirects with fragment' do + post provider, session: { user_return_to: '/fake/url' } + + expect(response).to redirect_to('/fake/url#L101') + end + end + + context 'when a redirect url with a fragment is stored' do + it 'redirects with the new fragment' do + post provider, session: { user_return_to: '/fake/url#replaceme' } + + expect(response).to redirect_to('/fake/url#L101') + end + end + + context 'when no redirect url is stored' do + it 'does not redirect with the fragment' do + post provider + + expect(response.redirect?).to be true + expect(response.location).not_to include('#L101') + end + end + end + context 'strategies' do context 'github' do let(:extern_uid) { 'my-uid' } diff --git a/spec/controllers/projects/error_tracking_controller_spec.rb b/spec/controllers/projects/error_tracking_controller_spec.rb new file mode 100644 index 00000000000..729e71b87a6 --- /dev/null +++ b/spec/controllers/projects/error_tracking_controller_spec.rb @@ -0,0 +1,142 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe Projects::ErrorTrackingController do + set(:project) { create(:project) } + set(:user) { create(:user) } + + before do + sign_in(user) + project.add_maintainer(user) + end + + describe 'GET #index' do + describe 'html' do + it 'renders index with 200 status code' do + get :index, params: project_params + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to render_template(:index) + end + + context 'with feature flag disabled' do + before do + stub_feature_flags(error_tracking: false) + end + + it 'returns 404' do + get :index, params: project_params + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context 'with insufficient permissions' do + before do + project.add_guest(user) + end + + it 'returns 404' do + get :index, params: project_params + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context 'with an anonymous user' do + before do + sign_out(user) + end + + it 'redirects to sign-in page' do + get :index, params: project_params + + expect(response).to redirect_to(new_user_session_path) + end + end + end + + describe 'format json' do + shared_examples 'no data' do + it 'returns no data' do + get :index, params: project_params(format: :json) + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to match_response_schema('error_tracking/index') + expect(json_response['external_url']).to be_nil + expect(json_response['errors']).to eq([]) + end + end + + let(:list_issues_service) { spy(:list_issues_service) } + let(:external_url) { 'http://example.com' } + + before do + expect(ErrorTracking::ListIssuesService) + .to receive(:new).with(project, user) + .and_return(list_issues_service) + end + + context 'service result is successful' do + before do + expect(list_issues_service).to receive(:execute) + .and_return(status: :success, issues: [error]) + expect(list_issues_service).to receive(:external_url) + .and_return(external_url) + end + + let(:error) { build(:error_tracking_error) } + + it 'returns a list of errors' do + get :index, params: project_params(format: :json) + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to match_response_schema('error_tracking/index') + expect(json_response['external_url']).to eq(external_url) + expect(json_response['errors']).to eq([error].as_json) + end + end + + context 'service result is erroneous' do + let(:error_message) { 'error message' } + + context 'without http_status' do + before do + expect(list_issues_service).to receive(:execute) + .and_return(status: :error, message: error_message) + end + + it 'returns 400 with message' do + get :index, params: project_params(format: :json) + + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response['message']).to eq(error_message) + end + end + + context 'with explicit http_status' do + let(:http_status) { :no_content } + + before do + expect(list_issues_service).to receive(:execute) + .and_return(status: :error, message: error_message, http_status: http_status) + end + + it 'returns http_status with message' do + get :index, params: project_params(format: :json) + + expect(response).to have_gitlab_http_status(http_status) + expect(json_response['message']).to eq(error_message) + end + end + end + end + end + + private + + def project_params(opts = {}) + opts.reverse_merge(namespace_id: project.namespace, project_id: project) + end +end diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb index df21dc7bc85..5b3256bf409 100644 --- a/spec/controllers/projects/issues_controller_spec.rb +++ b/spec/controllers/projects/issues_controller_spec.rb @@ -1030,19 +1030,6 @@ describe Projects::IssuesController do let(:project) { create(:project, :public) } let(:file) { fixture_file_upload('spec/fixtures/csv_comma.csv') } - context 'feature disabled' do - it 'returns 404' do - sign_in(user) - project.add_maintainer(user) - - stub_feature_flags(issues_import_csv: false) - - import_csv - - expect(response).to have_gitlab_http_status :not_found - end - end - context 'unauthorized' do it 'returns 404 for guests' do sign_out(:user) @@ -1086,9 +1073,9 @@ describe Projects::IssuesController do end def import_csv - post :import_csv, namespace_id: project.namespace.to_param, - project_id: project.to_param, - file: file + post :import_csv, params: { namespace_id: project.namespace.to_param, + project_id: project.to_param, + file: file } end end diff --git a/spec/controllers/projects/jobs_controller_spec.rb b/spec/controllers/projects/jobs_controller_spec.rb index 7f65fe551e9..d8a331b3cf0 100644 --- a/spec/controllers/projects/jobs_controller_spec.rb +++ b/spec/controllers/projects/jobs_controller_spec.rb @@ -770,50 +770,6 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do end end - describe 'POST cancel_all' do - before do - project.add_developer(user) - sign_in(user) - end - - context 'when jobs are cancelable' do - before do - create_list(:ci_build, 2, :cancelable, pipeline: pipeline) - - post_cancel_all - end - - it 'redirects to a index page' do - expect(response).to have_gitlab_http_status(:found) - expect(response).to redirect_to(namespace_project_jobs_path) - end - - it 'transits to canceled' do - expect(Ci::Build.all).to all(be_canceled) - end - end - - context 'when jobs are not cancelable' do - before do - create_list(:ci_build, 2, :canceled, pipeline: pipeline) - - post_cancel_all - end - - it 'redirects to a index page' do - expect(response).to have_gitlab_http_status(:found) - expect(response).to redirect_to(namespace_project_jobs_path) - end - end - - def post_cancel_all - post :cancel_all, params: { - namespace_id: project.namespace, - project_id: project - } - end - end - describe 'POST erase' do let(:role) { :maintainer } diff --git a/spec/controllers/projects/pages_controller_spec.rb b/spec/controllers/projects/pages_controller_spec.rb index 382c1b5d124..4b742a5d427 100644 --- a/spec/controllers/projects/pages_controller_spec.rb +++ b/spec/controllers/projects/pages_controller_spec.rb @@ -28,10 +28,10 @@ describe Projects::PagesController do let(:group) { create(:group, :nested) } let(:project) { create(:project, namespace: group) } - it 'returns a 404 status code' do + it 'returns a 200 status code' do get :show, params: request_params - expect(response).to have_gitlab_http_status(404) + expect(response).to have_gitlab_http_status(200) end end end diff --git a/spec/controllers/projects/releases_controller_spec.rb b/spec/controllers/projects/releases_controller_spec.rb index f170a2ab613..5b9d21d3d5b 100644 --- a/spec/controllers/projects/releases_controller_spec.rb +++ b/spec/controllers/projects/releases_controller_spec.rb @@ -6,10 +6,6 @@ describe Projects::ReleasesController do let!(:project) { create(:project, :repository, :public) } let!(:user) { create(:user) } - before do - stub_feature_flags(releases_page: true) - end - describe 'GET #index' do it 'renders a 200' do get_index @@ -43,18 +39,6 @@ describe Projects::ReleasesController do expect(response.status).to eq(404) end end - - context 'when releases_page feature flag is disabled' do - before do - stub_feature_flags(releases_page: false) - end - - it 'renders a 404' do - get_index - - expect(response.status).to eq(404) - end - end end private diff --git a/spec/controllers/projects/serverless/functions_controller_spec.rb b/spec/controllers/projects/serverless/functions_controller_spec.rb index a9759c4fbd8..87114d44bce 100644 --- a/spec/controllers/projects/serverless/functions_controller_spec.rb +++ b/spec/controllers/projects/serverless/functions_controller_spec.rb @@ -45,9 +45,45 @@ describe Projects::Serverless::FunctionsController do end end + describe 'GET #show' do + context 'invalid data' do + it 'has a bad function name' do + get :show, params: params({ format: :json, environment_id: "*", id: "foo" }) + expect(response).to have_gitlab_http_status(404) + end + end + + context 'valid data', :use_clean_rails_memory_store_caching do + before do + stub_kubeclient_service_pods + stub_reactive_cache(knative, + { + services: kube_knative_services_body(namespace: namespace.namespace, name: cluster.project.name)["items"], + pods: kube_knative_pods_body(cluster.project.name, namespace.namespace)["items"] + }) + end + + it 'has a valid function name' do + get :show, params: params({ format: :json, environment_id: "*", id: cluster.project.name }) + expect(response).to have_gitlab_http_status(200) + + expect(json_response).to include( + "name" => project.name, + "url" => "http://#{project.name}.#{namespace.namespace}.example.com", + "podcount" => 1 + ) + end + end + end + describe 'GET #index with data', :use_clean_rails_memory_store_caching do before do - stub_reactive_cache(knative, services: kube_knative_services_body(namespace: namespace.namespace, name: cluster.project.name)["items"]) + stub_kubeclient_service_pods + stub_reactive_cache(knative, + { + services: kube_knative_services_body(namespace: namespace.namespace, name: cluster.project.name)["items"], + pods: kube_knative_pods_body(cluster.project.name, namespace.namespace)["items"] + }) end it 'has data' do diff --git a/spec/factories/error_tracking/error.rb b/spec/factories/error_tracking/error.rb new file mode 100644 index 00000000000..ff883a3d22c --- /dev/null +++ b/spec/factories/error_tracking/error.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :error_tracking_error, class: Gitlab::ErrorTracking::Error do + id 'id' + title 'title' + type 'error' + user_count 1 + count 2 + first_seen { Time.now } + last_seen { Time.now } + message 'message' + culprit 'culprit' + external_url 'http://example.com/id' + project_id 'project1' + project_name 'project name' + project_slug 'project_name' + short_id 'ID' + status 'unresolved' + frequency [] + + skip_create + end +end diff --git a/spec/factories/project_error_tracking_settings.rb b/spec/factories/project_error_tracking_settings.rb index f044cbe8755..fbd8dfd395c 100644 --- a/spec/factories/project_error_tracking_settings.rb +++ b/spec/factories/project_error_tracking_settings.rb @@ -3,7 +3,7 @@ FactoryBot.define do factory :project_error_tracking_setting, class: ErrorTracking::ProjectErrorTrackingSetting do project - api_url 'https://gitlab.com' + api_url 'https://gitlab.com/api/0/projects/sentry-org/sentry-project' enabled true token 'access_token_123' end diff --git a/spec/factories/wiki_pages.rb b/spec/factories/wiki_pages.rb index 2335b5118dd..ae257d769e8 100644 --- a/spec/factories/wiki_pages.rb +++ b/spec/factories/wiki_pages.rb @@ -5,7 +5,7 @@ FactoryBot.define do transient do attrs do { - title: 'Title', + title: 'Title.with.dot', content: 'Content for wiki page', format: 'markdown' } diff --git a/spec/features/admin/admin_settings_spec.rb b/spec/features/admin/admin_settings_spec.rb index 0a69a26eb3e..04f39b807d7 100644 --- a/spec/features/admin/admin_settings_spec.rb +++ b/spec/features/admin/admin_settings_spec.rb @@ -238,7 +238,7 @@ describe 'Admin updates settings' do page.within('.as-ci-cd') do check 'Default to Auto DevOps pipeline for all projects' - fill_in 'Auto devops domain', with: 'domain.com' + fill_in 'application_setting_auto_devops_domain', with: 'domain.com' click_button 'Save changes' end diff --git a/spec/features/boards/boards_spec.rb b/spec/features/boards/boards_spec.rb index baa2b1d8af5..08c27354bd2 100644 --- a/spec/features/boards/boards_spec.rb +++ b/spec/features/boards/boards_spec.rb @@ -97,7 +97,7 @@ describe 'Issue Boards', :js do expect(find('.board:nth-child(4)')).to have_selector('.board-card') end - it 'shows description tooltip on list title' do + it 'shows description tooltip on list title', :quarantine do page.within('.board:nth-child(2)') do expect(find('.board-title span.has-tooltip')[:title]).to eq('Test') end @@ -411,7 +411,7 @@ describe 'Issue Boards', :js do wait_for_empty_boards((2..4)) end - it 'filters by label with space after reload' do + it 'filters by label with space after reload', :quarantine do set_filter("label", "\"#{accepting.title}") click_filter_link(accepting.title) submit_filter @@ -477,7 +477,7 @@ describe 'Issue Boards', :js do end end - it 'filters by multiple labels' do + it 'filters by multiple labels', :quarantine do set_filter("label", testing.title) click_filter_link(testing.title) diff --git a/spec/features/dashboard/help_spec.rb b/spec/features/dashboard/help_spec.rb index 68bfbf22736..fa12cecc984 100644 --- a/spec/features/dashboard/help_spec.rb +++ b/spec/features/dashboard/help_spec.rb @@ -5,13 +5,23 @@ RSpec.describe 'Dashboard Help' do sign_in(create(:user)) end - it 'renders correctly markdown' do - visit help_page_path("administration/raketasks/maintenance") + context 'help dropdown' do + it 'shows the "What\'s new?" menu item' do + visit root_dashboard_path - expect(page).to have_content('Gather information about GitLab and the system it runs on') + expect(page.find('.header-help .dropdown-menu')).to have_text("What's new?") + end + end + + context 'documentation' do + it 'renders correctly markdown' do + visit help_page_path("administration/raketasks/maintenance") + + expect(page).to have_content('Gather information about GitLab and the system it runs on') - node = find('.documentation h2 a#user-content-check-gitlab-configuration') - expect(node[:href]).to eq '#check-gitlab-configuration' - expect(find(:xpath, "#{node.path}/..").text).to eq 'Check GitLab configuration' + node = find('.documentation h2 a#user-content-check-gitlab-configuration') + expect(node[:href]).to eq '#check-gitlab-configuration' + expect(find(:xpath, "#{node.path}/..").text).to eq 'Check GitLab configuration' + end end end diff --git a/spec/features/dashboard/projects_spec.rb b/spec/features/dashboard/projects_spec.rb index 975b7944741..edca8f9df08 100644 --- a/spec/features/dashboard/projects_spec.rb +++ b/spec/features/dashboard/projects_spec.rb @@ -91,6 +91,7 @@ describe 'Dashboard Projects' do visit dashboard_projects_path expect(page).to have_content(project.name) + expect(find('.nav-links li:nth-child(1) .badge-pill')).to have_content(1) end it 'shows personal projects on personal projects tab', :js do @@ -121,6 +122,8 @@ describe 'Dashboard Projects' do expect(page).not_to have_content(project.name) expect(page).to have_content(project2.name) + 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 end diff --git a/spec/features/dashboard/shortcuts_spec.rb b/spec/features/dashboard/shortcuts_spec.rb index 31a1dcf826d..cbddf117462 100644 --- a/spec/features/dashboard/shortcuts_spec.rb +++ b/spec/features/dashboard/shortcuts_spec.rb @@ -49,7 +49,7 @@ describe 'Dashboard shortcuts', :js do find('body').send_keys([:shift, 'P']) find('.nothing-here-block') - expect(page).to have_content('No projects found') + expect(page).to have_content('This user doesn\'t have any personal projects') end end diff --git a/spec/features/dashboard/todos/todos_spec.rb b/spec/features/dashboard/todos/todos_spec.rb index 96b22a0f64b..2284ee925a0 100644 --- a/spec/features/dashboard/todos/todos_spec.rb +++ b/spec/features/dashboard/todos/todos_spec.rb @@ -332,7 +332,7 @@ describe 'Dashboard Todos' do it 'links to the pipelines for the merge request' do href = pipelines_project_merge_request_path(project, todo.target) - expect(page).to have_link "merge request #{todo.target.to_reference(full: true)}", href + expect(page).to have_link "merge request #{todo.target.to_reference(full: true)}", href: href end end end diff --git a/spec/features/dashboard/user_filters_projects_spec.rb b/spec/features/dashboard/user_filters_projects_spec.rb index 3746d37b9cd..cc86114e436 100644 --- a/spec/features/dashboard/user_filters_projects_spec.rb +++ b/spec/features/dashboard/user_filters_projects_spec.rb @@ -38,7 +38,7 @@ describe 'Dashboard > User filters projects' do it 'returns message when starred projects fitler returns no results' do fill_in 'project-filter-form-field', with: 'Beta\n' - expect(page).to have_content('No projects found') + expect(page).to have_content('This user doesn\'t have any personal projects') expect(page).not_to have_content('You don\'t have starred projects yet') end end diff --git a/spec/features/groups/empty_states_spec.rb b/spec/features/groups/empty_states_spec.rb index 8f5ca781b2c..e4eb0d355d1 100644 --- a/spec/features/groups/empty_states_spec.rb +++ b/spec/features/groups/empty_states_spec.rb @@ -23,14 +23,52 @@ describe 'Group empty states' do end context "the project has #{issuable_name}s" do - before do + it 'does not display an empty state' do create(issuable, project_relation => project) visit path + expect(page).not_to have_selector('.empty-state') end - it 'does not display an empty state' do - expect(page).not_to have_selector('.empty-state') + it "displays link to create new #{issuable} when no open #{issuable} is found" do + create("closed_#{issuable}", project_relation => project) + issuable_link_fn = "project_#{issuable}s_path" + + visit public_send(issuable_link_fn, project) + + page.within(find('.empty-state')) do + expect(page).to have_content(/There are no open #{issuable.to_s.humanize.downcase}/) + expect(page).to have_selector("#new_#{issuable}_body_link") + end + end + + it 'displays link to create new issue when the current search gave no results' do + create(issuable, project_relation => project) + + issuable_link_fn = "project_#{issuable}s_path" + + visit public_send(issuable_link_fn, project, author_username: 'foo', scope: 'all', state: 'opened') + + page.within(find('.empty-state')) do + expect(page).to have_content(/Sorry, your filter produced no results/) + new_issuable_path = issuable == :issue ? 'new_project_issue_path' : 'project_new_merge_request_path' + + path = public_send(new_issuable_path, project) + + expect(page).to have_selector("#new_#{issuable}_body_link[href='#{path}']") + end + end + + it "displays conditional text when no closed #{issuable} is found" do + create(issuable, project_relation => project) + + issuable_link_fn = "project_#{issuable}s_path" + + visit public_send(issuable_link_fn, project, state: 'closed') + + page.within(find('.empty-state')) do + expect(page).to have_content(/There are no closed #{issuable.to_s.humanize.downcase}/) + end end end diff --git a/spec/features/merge_request/user_merges_immediately_spec.rb b/spec/features/merge_request/user_merges_immediately_spec.rb index ea61f9675bc..84636ae355c 100644 --- a/spec/features/merge_request/user_merges_immediately_spec.rb +++ b/spec/features/merge_request/user_merges_immediately_spec.rb @@ -25,6 +25,8 @@ describe 'Merge requests > User merges immediately', :js do end it 'enables merge immediately' do + wait_for_requests + page.within '.mr-widget-body' do find('.dropdown-toggle').click diff --git a/spec/features/merge_request/user_sees_mini_pipeline_graph_spec.rb b/spec/features/merge_request/user_sees_mini_pipeline_graph_spec.rb index 0959f1b12f3..5188dc3625f 100644 --- a/spec/features/merge_request/user_sees_mini_pipeline_graph_spec.rb +++ b/spec/features/merge_request/user_sees_mini_pipeline_graph_spec.rb @@ -51,22 +51,52 @@ describe 'Merge request < User sees mini pipeline graph', :js do first('.mini-pipeline-graph-dropdown-toggle') end - it 'expands when hovered' do + # Status icon button styles should update as described in + # https://gitlab.com/gitlab-org/gitlab-ce/issues/42769 + it 'has unique styles for default, :hover, :active, and :focus states' do find('.mini-pipeline-graph-dropdown-toggle') - before_width = evaluate_script("$('.mini-pipeline-graph-dropdown-toggle:visible').outerWidth();") + default_background_color = evaluate_script("$('.mini-pipeline-graph-dropdown-toggle:visible').css('background-color');") + default_foreground_color = evaluate_script("$('.mini-pipeline-graph-dropdown-toggle:visible svg').css('fill');") + default_box_shadow = evaluate_script("$('.mini-pipeline-graph-dropdown-toggle:visible').css('box-shadow');") toggle.hover find('.mini-pipeline-graph-dropdown-toggle') - after_width = evaluate_script("$('.mini-pipeline-graph-dropdown-toggle:visible').outerWidth();") + hover_background_color = evaluate_script("$('.mini-pipeline-graph-dropdown-toggle:visible').css('background-color');") + hover_foreground_color = evaluate_script("$('.mini-pipeline-graph-dropdown-toggle:visible svg').css('fill');") + hover_box_shadow = evaluate_script("$('.mini-pipeline-graph-dropdown-toggle:visible').css('box-shadow');") - expect(before_width).to be < after_width - end + page.driver.browser.action.click_and_hold(toggle.native).perform - it 'shows dropdown caret when hovered' do - toggle.hover + find('.mini-pipeline-graph-dropdown-toggle') + active_background_color = evaluate_script("$('.mini-pipeline-graph-dropdown-toggle:visible').css('background-color');") + active_foreground_color = evaluate_script("$('.mini-pipeline-graph-dropdown-toggle:visible svg').css('fill');") + active_box_shadow = evaluate_script("$('.mini-pipeline-graph-dropdown-toggle:visible').css('box-shadow');") + + page.driver.browser.action.release(toggle.native) + .move_by(100, 100) + .perform + + find('.mini-pipeline-graph-dropdown-toggle') + focus_background_color = evaluate_script("$('.mini-pipeline-graph-dropdown-toggle:visible').css('background-color');") + focus_foreground_color = evaluate_script("$('.mini-pipeline-graph-dropdown-toggle:visible svg').css('fill');") + focus_box_shadow = evaluate_script("$('.mini-pipeline-graph-dropdown-toggle:visible').css('box-shadow');") + + expect(default_background_color).not_to eq(hover_background_color) + expect(hover_background_color).not_to eq(active_background_color) + expect(default_background_color).not_to eq(active_background_color) + + expect(default_foreground_color).not_to eq(hover_foreground_color) + expect(hover_foreground_color).not_to eq(active_foreground_color) + expect(default_foreground_color).not_to eq(active_foreground_color) + + expect(focus_background_color).to eq(hover_background_color) + expect(focus_foreground_color).to eq(hover_foreground_color) - expect(toggle).to have_selector('.fa-caret-down') + expect(default_box_shadow).to eq('none') + expect(hover_box_shadow).to eq('none') + expect(active_box_shadow).not_to eq('none') + expect(focus_box_shadow).not_to eq('none') end it 'shows tooltip when hovered' do diff --git a/spec/features/projects/artifacts/user_browses_artifacts_spec.rb b/spec/features/projects/artifacts/user_browses_artifacts_spec.rb index 9ebbbaea911..5f630c9ffa4 100644 --- a/spec/features/projects/artifacts/user_browses_artifacts_spec.rb +++ b/spec/features/projects/artifacts/user_browses_artifacts_spec.rb @@ -25,8 +25,8 @@ describe "User browses artifacts" do page.within(".tree-table") do expect(page).to have_no_content("..") .and have_content("other_artifacts_0.1.2") - .and have_content("ci_artifacts.txt") - .and have_content("rails_sample.jpg") + .and have_content("ci_artifacts.txt 27 Bytes") + .and have_content("rails_sample.jpg 34.4 KB") end page.within(".build-header") do diff --git a/spec/features/projects/compare_spec.rb b/spec/features/projects/compare_spec.rb index 69600884909..5f7cf68987e 100644 --- a/spec/features/projects/compare_spec.rb +++ b/spec/features/projects/compare_spec.rb @@ -87,6 +87,21 @@ describe "Compare", :js do expect(find(".js-compare-from-dropdown .dropdown-content")).to have_selector("li", count: 3) end + + context 'when commit has overflow', :js do + it 'displays warning' do + visit project_compare_index_path(project, from: "feature", to: "master") + + allow(Commit).to receive(:max_diff_options).and_return(max_files: 3) + allow_any_instance_of(DiffHelper).to receive(:render_overflow_warning?).and_return(true) + + click_button('Compare') + + page.within('.alert') do + expect(page).to have_text("Too many changes to show. To preserve performance only 3 of 3+ files are displayed.") + end + end + end end describe "tags" do diff --git a/spec/features/projects/jobs_spec.rb b/spec/features/projects/jobs_spec.rb index 60f37f4b74a..8230396a4cc 100644 --- a/spec/features/projects/jobs_spec.rb +++ b/spec/features/projects/jobs_spec.rb @@ -28,7 +28,6 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do end it "shows Pending tab jobs" do - expect(page).to have_link 'Cancel running' expect(page).to have_selector('.nav-links li.active', text: 'Pending') expect(page).to have_content job.short_sha expect(page).to have_content job.ref @@ -44,7 +43,6 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do it "shows Running tab jobs" do expect(page).to have_selector('.nav-links li.active', text: 'Running') - expect(page).to have_link 'Cancel running' expect(page).to have_content job.short_sha expect(page).to have_content job.ref expect(page).to have_content job.name @@ -60,7 +58,6 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do it "shows Finished tab jobs" do expect(page).to have_selector('.nav-links li.active', text: 'Finished') expect(page).to have_content 'No jobs to show' - expect(page).to have_link 'Cancel running' end end @@ -75,7 +72,6 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do expect(page).to have_content job.short_sha expect(page).to have_content job.ref expect(page).to have_content job.name - expect(page).not_to have_link 'Cancel running' end end @@ -94,23 +90,6 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do end end - describe "POST /:project/jobs/:id/cancel_all" do - before do - job.run! - visit project_jobs_path(project) - click_link "Cancel running" - end - - it 'shows all necessary content' do - expect(page).to have_selector('.nav-links li.active', text: 'All') - expect(page).to have_content 'canceled' - expect(page).to have_content job.short_sha - expect(page).to have_content job.ref - expect(page).to have_content job.name - expect(page).not_to have_link 'Cancel running' - end - end - describe "GET /:project/jobs/:id" do context "Job from project" do let(:job) { create(:ci_build, :success, :trace_live, pipeline: pipeline) } @@ -191,7 +170,7 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do href = new_project_issue_path(project, options) - page.within('.header-action-buttons') do + page.within('.build-sidebar') do expect(find('.js-new-issue')['href']).to include(href) end end diff --git a/spec/features/projects/pipelines/pipeline_spec.rb b/spec/features/projects/pipelines/pipeline_spec.rb index 4706c28bb3d..3192c9ffad4 100644 --- a/spec/features/projects/pipelines/pipeline_spec.rb +++ b/spec/features/projects/pipelines/pipeline_spec.rb @@ -477,10 +477,11 @@ describe 'Pipeline', :js do end context 'when accessing failed jobs page' do - it 'fails to access the page' do - subject + it 'renders a 404 page' do + requests = inspect_requests { subject } - expect(page).to have_title('Access Denied') + expect(page).to have_title('Not Found') + expect(requests.first.status_code).to eq(404) end end end diff --git a/spec/features/projects/settings/user_tags_project_spec.rb b/spec/features/projects/settings/user_tags_project_spec.rb index 9357215ae6f..e3f06c042b9 100644 --- a/spec/features/projects/settings/user_tags_project_spec.rb +++ b/spec/features/projects/settings/user_tags_project_spec.rb @@ -9,13 +9,13 @@ describe 'Projects > Settings > User tags a project' do visit edit_project_path(project) end - it 'sets project tags' do - fill_in 'Tags', with: 'tag1, tag2' + it 'sets project topics' do + fill_in 'Topics', with: 'topic1, topic2' page.within '.general-settings' do click_button 'Save changes' end - expect(find_field('Tags').value).to eq 'tag1, tag2' + expect(find_field('Topics').value).to eq 'topic1, topic2' end end diff --git a/spec/features/projects_spec.rb b/spec/features/projects_spec.rb index b56bb272b46..eb70a3c41c1 100644 --- a/spec/features/projects_spec.rb +++ b/spec/features/projects_spec.rb @@ -99,6 +99,30 @@ describe 'Project' do end end + describe 'project topics' do + let(:project) { create(:project, :repository) } + let(:path) { project_path(project) } + + before do + sign_in(create(:admin)) + visit path + end + + it 'shows project topics' do + project.update_attribute(:tag_list, 'topic1') + visit path + expect(page).to have_css('.project-topic-list') + expect(page).to have_content('topic1') + end + + it 'shows up to 3 project tags' do + project.update_attribute(:tag_list, 'topic1, topic2, topic3, topic4') + visit path + expect(page).to have_css('.project-topic-list') + expect(page).to have_content('topic1, topic2, topic3 + 1 more') + end + end + describe 'copy clone URL to clipboard', :js do let(:project) { create(:project, :repository) } let(:path) { project_path(project) } diff --git a/spec/features/runners_spec.rb b/spec/features/runners_spec.rb index 09de983f669..6eae0be4b9f 100644 --- a/spec/features/runners_spec.rb +++ b/spec/features/runners_spec.rb @@ -236,7 +236,7 @@ describe 'Runners' do it 'group runners are available' do visit project_runners_path(project) - expect(page).to have_content 'Available group Runners : 1' + expect(page).to have_content 'Available group Runners: 1' expect(page).to have_content 'group-runner' end @@ -279,7 +279,7 @@ describe 'Runners' do visit group_settings_ci_cd_path(group) expect(page).not_to have_content 'This group does not provide any group Runners yet' - expect(page).to have_content 'Available group Runners : 1' + expect(page).to have_content 'Available group Runners: 1' expect(page).to have_content 'group-runner' end diff --git a/spec/features/users/overview_spec.rb b/spec/features/users/overview_spec.rb index 8748230fa0c..3708f0ee477 100644 --- a/spec/features/users/overview_spec.rb +++ b/spec/features/users/overview_spec.rb @@ -96,7 +96,7 @@ describe 'Overview tab on a user profile', :js do it 'it shows an empty project list with an info message' do page.within('.projects-block') do expect(page).to have_selector('.loading', visible: false) - expect(page).to have_content('No projects found') + expect(page).to have_content('This user doesn\'t have any personal projects') expect(page).not_to have_selector('.project-row') end end diff --git a/spec/finders/issues_finder_spec.rb b/spec/finders/issues_finder_spec.rb index 80f7232f282..682fae06434 100644 --- a/spec/finders/issues_finder_spec.rb +++ b/spec/finders/issues_finder_spec.rb @@ -174,9 +174,13 @@ describe IssuesFinder do context 'filtering by upcoming milestone' do let(:params) { { milestone_title: Milestone::Upcoming.name } } + let!(:group) { create(:group, :public) } + let!(:group_member) { create(:group_member, group: group, user: user) } + let(:project_no_upcoming_milestones) { create(:project, :public) } let(:project_next_1_1) { create(:project, :public) } let(:project_next_8_8) { create(:project, :public) } + let(:project_in_group) { create(:project, :public, namespace: group) } let(:yesterday) { Date.today - 1.day } let(:tomorrow) { Date.today + 1.day } @@ -187,21 +191,22 @@ describe IssuesFinder do [ create(:milestone, :closed, project: project_no_upcoming_milestones), create(:milestone, project: project_next_1_1, title: '1.1', due_date: two_days_from_now), - create(:milestone, project: project_next_1_1, title: '8.8', due_date: ten_days_from_now), - create(:milestone, project: project_next_8_8, title: '1.1', due_date: yesterday), - create(:milestone, project: project_next_8_8, title: '8.8', due_date: tomorrow) + create(:milestone, project: project_next_1_1, title: '8.9', due_date: ten_days_from_now), + create(:milestone, project: project_next_8_8, title: '1.2', due_date: yesterday), + create(:milestone, project: project_next_8_8, title: '8.8', due_date: tomorrow), + create(:milestone, group: group, title: '9.9', due_date: tomorrow) ] end before do milestones.each do |milestone| - create(:issue, project: milestone.project, milestone: milestone, author: user, assignees: [user]) + create(:issue, project: milestone.project || project_in_group, milestone: milestone, author: user, assignees: [user]) end end - it 'returns issues in the upcoming milestone for each project' do - expect(issues.map { |issue| issue.milestone.title }).to contain_exactly('1.1', '8.8') - expect(issues.map { |issue| issue.milestone.due_date }).to contain_exactly(tomorrow, two_days_from_now) + it 'returns issues in the upcoming milestone for each project or group' do + expect(issues.map { |issue| issue.milestone.title }).to contain_exactly('1.1', '8.8', '9.9') + expect(issues.map { |issue| issue.milestone.due_date }).to contain_exactly(tomorrow, two_days_from_now, tomorrow) end end diff --git a/spec/finders/projects/serverless/functions_finder_spec.rb b/spec/finders/projects/serverless/functions_finder_spec.rb index 60d02b12054..35279906854 100644 --- a/spec/finders/projects/serverless/functions_finder_spec.rb +++ b/spec/finders/projects/serverless/functions_finder_spec.rb @@ -29,15 +29,34 @@ describe Projects::Serverless::FunctionsFinder do context 'has knative installed' do let!(:knative) { create(:clusters_applications_knative, :installed, cluster: cluster) } + let(:finder) { described_class.new(project.clusters) } it 'there are no functions' do - expect(described_class.new(project.clusters).execute).to be_empty + expect(finder.execute).to be_empty end it 'there are functions', :use_clean_rails_memory_store_caching do - stub_reactive_cache(knative, services: kube_knative_services_body(namespace: namespace.namespace, name: cluster.project.name)["items"]) + stub_kubeclient_service_pods + stub_reactive_cache(knative, + { + services: kube_knative_services_body(namespace: namespace.namespace, name: cluster.project.name)["items"], + pods: kube_knative_pods_body(cluster.project.name, namespace.namespace)["items"] + }) - expect(described_class.new(project.clusters).execute).not_to be_empty + expect(finder.execute).not_to be_empty + end + + it 'has a function', :use_clean_rails_memory_store_caching do + stub_kubeclient_service_pods + stub_reactive_cache(knative, + { + services: kube_knative_services_body(namespace: namespace.namespace, name: cluster.project.name)["items"], + pods: kube_knative_pods_body(cluster.project.name, namespace.namespace)["items"] + }) + + result = finder.service(cluster.environment_scope, cluster.project.name) + expect(result).not_to be_empty + expect(result["metadata"]["name"]).to be_eql(cluster.project.name) end end end diff --git a/spec/fixtures/api/schemas/error_tracking/error.json b/spec/fixtures/api/schemas/error_tracking/error.json new file mode 100644 index 00000000000..df2c02d7d5d --- /dev/null +++ b/spec/fixtures/api/schemas/error_tracking/error.json @@ -0,0 +1,21 @@ +{ + "type": "object", + "required" : [ + "external_url", + "last_seen", + "message", + "type" + ], + "properties" : { + "id": { "type": "string"}, + "first_seen": { "type": "string", "format": "date-time" }, + "last_seen": { "type": "string", "format": "date-time" }, + "type": { "type": "string" }, + "message": { "type": "string" }, + "culprit": { "type": "string" }, + "count": { "type": "integer"}, + "external_url": { "type": "string" }, + "user_count": { "type": "integer"} + }, + "additionalProperties": true +} diff --git a/spec/fixtures/api/schemas/error_tracking/index.json b/spec/fixtures/api/schemas/error_tracking/index.json new file mode 100644 index 00000000000..d3abc29ffa7 --- /dev/null +++ b/spec/fixtures/api/schemas/error_tracking/index.json @@ -0,0 +1,15 @@ +{ + "type": "object", + "required": [ + "external_url", + "errors" + ], + "properties": { + "external_url": { "type": ["string", "null"] }, + "errors": { + "type": "array", + "items": { "$ref": "error.json" } + } + }, + "additionalProperties": false +} diff --git a/spec/fixtures/malicious.bundle b/spec/fixtures/malicious.bundle new file mode 100644 index 00000000000..7ba47932906 --- /dev/null +++ b/spec/fixtures/malicious.bundle @@ -0,0 +1 @@ +gitdir: foo.git diff --git a/spec/fixtures/security-reports/feature-branch/gl-container-scanning-report.json b/spec/fixtures/security-reports/feature-branch/gl-container-scanning-report.json index 9840382df6f..6f89d20d4bf 100644 --- a/spec/fixtures/security-reports/feature-branch/gl-container-scanning-report.json +++ b/spec/fixtures/security-reports/feature-branch/gl-container-scanning-report.json @@ -1,18 +1,16 @@ { - "image": "registry.gitlab.com/bikebilly/auto-devops-10-6/feature-branch:e7315ba964febb11bac8f5cd6ec433db8a3a1583", - "unapproved": [ - "CVE-2017-15650" - ], - "vulnerabilities": [ - { - "featurename": "musl", - "featureversion": "1.1.14-r15", - "vulnerability": "CVE-2017-15650", - "namespace": "alpine:v3.4", - "description": "", - "link": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-15650", - "severity": "Medium", - "fixedby": "1.1.14-r16" - } - ] + "image": "registry.gitlab.com/bikebilly/auto-devops-10-6/feature-branch:e7315ba964febb11bac8f5cd6ec433db8a3a1583", + "unapproved": ["CVE-2017-15650"], + "vulnerabilities": [ + { + "featurename": "musl", + "featureversion": "1.1.14-r15", + "vulnerability": "CVE-2017-15650", + "namespace": "alpine:v3.4", + "description": "", + "link": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-15650", + "severity": "Medium", + "fixedby": "1.1.14-r16" + } + ] } diff --git a/spec/fixtures/security-reports/master/gl-container-scanning-report.json b/spec/fixtures/security-reports/master/gl-container-scanning-report.json index c087352a122..68c6099836b 100644 --- a/spec/fixtures/security-reports/master/gl-container-scanning-report.json +++ b/spec/fixtures/security-reports/master/gl-container-scanning-report.json @@ -1,92 +1,92 @@ { - "image": "registry.gitlab.com/groulot/container-scanning-test/master:5f21de6956aee99ddb68ae49498662d9872f50ff", - "unapproved": [ - "CVE-2017-18018", - "CVE-2016-2781", - "CVE-2017-12424", - "CVE-2007-5686", - "CVE-2013-4235" - ], - "vulnerabilities": [ - { - "featurename": "glibc", - "featureversion": "2.24-11+deb9u3", - "vulnerability": "CVE-2017-18269", - "namespace": "debian:9", - "description": "SSE2-optimized memmove implementation problem.", - "link": "https://security-tracker.debian.org/tracker/CVE-2017-18269", - "severity": "Defcon1", - "fixedby": "2.24-11+deb9u4" - }, - { - "featurename": "glibc", - "featureversion": "2.24-11+deb9u3", - "vulnerability": "CVE-2017-16997", - "namespace": "debian:9", - "description": "elf/dl-load.c in the GNU C Library (aka glibc or libc6) 2.19 through 2.26 mishandles RPATH and RUNPATH containing $ORIGIN for a privileged (setuid or AT_SECURE) program, which allows local users to gain privileges via a Trojan horse library in the current working directory, related to the fillin_rpath and decompose_rpath functions. This is associated with misinterpretion of an empty RPATH/RUNPATH token as the \"./\" directory. NOTE: this configuration of RPATH/RUNPATH for a privileged program is apparently very uncommon; most likely, no such program is shipped with any common Linux distribution.", - "link": "https://security-tracker.debian.org/tracker/CVE-2017-16997", - "severity": "Critical", - "fixedby": "" - }, - { - "featurename": "glibc", - "featureversion": "2.24-11+deb9u3", - "vulnerability": "CVE-2018-1000001", - "namespace": "debian:9", - "description": "In glibc 2.26 and earlier there is confusion in the usage of getcwd() by realpath() which can be used to write before the destination buffer leading to a buffer underflow and potential code execution.", - "link": "https://security-tracker.debian.org/tracker/CVE-2018-1000001", - "severity": "High", - "fixedby": "" - }, - { - "featurename": "glibc", - "featureversion": "2.24-11+deb9u3", - "vulnerability": "CVE-2016-10228", - "namespace": "debian:9", - "description": "The iconv program in the GNU C Library (aka glibc or libc6) 2.25 and earlier, when invoked with the -c option, enters an infinite loop when processing invalid multi-byte input sequences, leading to a denial of service.", - "link": "https://security-tracker.debian.org/tracker/CVE-2016-10228", - "severity": "Medium", - "fixedby": "" - }, - { - "featurename": "elfutils", - "featureversion": "0.168-1", - "vulnerability": "CVE-2018-18520", - "namespace": "debian:9", - "description": "An Invalid Memory Address Dereference exists in the function elf_end in libelf in elfutils through v0.174. Although eu-size is intended to support ar files inside ar files, handle_ar in size.c closes the outer ar file before handling all inner entries. The vulnerability allows attackers to cause a denial of service (application crash) with a crafted ELF file.", - "link": "https://security-tracker.debian.org/tracker/CVE-2018-18520", - "severity": "Low", - "fixedby": "" - }, - { - "featurename": "glibc", - "featureversion": "2.24-11+deb9u3", - "vulnerability": "CVE-2010-4052", - "namespace": "debian:9", - "description": "Stack consumption vulnerability in the regcomp implementation in the GNU C Library (aka glibc or libc6) through 2.11.3, and 2.12.x through 2.12.2, allows context-dependent attackers to cause a denial of service (resource exhaustion) via a regular expression containing adjacent repetition operators, as demonstrated by a {10,}{10,}{10,}{10,} sequence in the proftpd.gnu.c exploit for ProFTPD.", - "link": "https://security-tracker.debian.org/tracker/CVE-2010-4052", - "severity": "Negligible", - "fixedby": "" - }, - { - "featurename": "nettle", - "featureversion": "3.3-1", - "vulnerability": "CVE-2018-16869", - "namespace": "debian:9", - "description": "A Bleichenbacher type side-channel based padding oracle attack was found in the way nettle handles endian conversion of RSA decrypted PKCS#1 v1.5 data. An attacker who is able to run a process on the same physical core as the victim process, could use this flaw extract plaintext or in some cases downgrade any TLS connections to a vulnerable server.", - "link": "https://security-tracker.debian.org/tracker/CVE-2018-16869", - "severity": "Unknown", - "fixedby": "" - }, - { - "featurename": "perl", - "featureversion": "5.24.1-3+deb9u4", - "vulnerability": "CVE-2018-18311", - "namespace": "debian:9", - "description": "Perl before 5.26.3 and 5.28.x before 5.28.1 has a buffer overflow via a crafted regular expression that triggers invalid write operations.", - "link": "https://security-tracker.debian.org/tracker/CVE-2018-18311", - "severity": "Unknown", - "fixedby": "5.24.1-3+deb9u5" - } - ] -}
\ No newline at end of file + "image": "registry.gitlab.com/groulot/container-scanning-test/master:5f21de6956aee99ddb68ae49498662d9872f50ff", + "unapproved": [ + "CVE-2017-18018", + "CVE-2016-2781", + "CVE-2017-12424", + "CVE-2007-5686", + "CVE-2013-4235" + ], + "vulnerabilities": [ + { + "featurename": "glibc", + "featureversion": "2.24-11+deb9u3", + "vulnerability": "CVE-2017-18269", + "namespace": "debian:9", + "description": "SSE2-optimized memmove implementation problem.", + "link": "https://security-tracker.debian.org/tracker/CVE-2017-18269", + "severity": "Defcon1", + "fixedby": "2.24-11+deb9u4" + }, + { + "featurename": "glibc", + "featureversion": "2.24-11+deb9u3", + "vulnerability": "CVE-2017-16997", + "namespace": "debian:9", + "description": "elf/dl-load.c in the GNU C Library (aka glibc or libc6) 2.19 through 2.26 mishandles RPATH and RUNPATH containing $ORIGIN for a privileged (setuid or AT_SECURE) program, which allows local users to gain privileges via a Trojan horse library in the current working directory, related to the fillin_rpath and decompose_rpath functions. This is associated with misinterpretion of an empty RPATH/RUNPATH token as the \"./\" directory. NOTE: this configuration of RPATH/RUNPATH for a privileged program is apparently very uncommon; most likely, no such program is shipped with any common Linux distribution.", + "link": "https://security-tracker.debian.org/tracker/CVE-2017-16997", + "severity": "Critical", + "fixedby": "" + }, + { + "featurename": "glibc", + "featureversion": "2.24-11+deb9u3", + "vulnerability": "CVE-2018-1000001", + "namespace": "debian:9", + "description": "In glibc 2.26 and earlier there is confusion in the usage of getcwd() by realpath() which can be used to write before the destination buffer leading to a buffer underflow and potential code execution.", + "link": "https://security-tracker.debian.org/tracker/CVE-2018-1000001", + "severity": "High", + "fixedby": "" + }, + { + "featurename": "glibc", + "featureversion": "2.24-11+deb9u3", + "vulnerability": "CVE-2016-10228", + "namespace": "debian:9", + "description": "The iconv program in the GNU C Library (aka glibc or libc6) 2.25 and earlier, when invoked with the -c option, enters an infinite loop when processing invalid multi-byte input sequences, leading to a denial of service.", + "link": "https://security-tracker.debian.org/tracker/CVE-2016-10228", + "severity": "Medium", + "fixedby": "" + }, + { + "featurename": "elfutils", + "featureversion": "0.168-1", + "vulnerability": "CVE-2018-18520", + "namespace": "debian:9", + "description": "An Invalid Memory Address Dereference exists in the function elf_end in libelf in elfutils through v0.174. Although eu-size is intended to support ar files inside ar files, handle_ar in size.c closes the outer ar file before handling all inner entries. The vulnerability allows attackers to cause a denial of service (application crash) with a crafted ELF file.", + "link": "https://security-tracker.debian.org/tracker/CVE-2018-18520", + "severity": "Low", + "fixedby": "" + }, + { + "featurename": "glibc", + "featureversion": "2.24-11+deb9u3", + "vulnerability": "CVE-2010-4052", + "namespace": "debian:9", + "description": "Stack consumption vulnerability in the regcomp implementation in the GNU C Library (aka glibc or libc6) through 2.11.3, and 2.12.x through 2.12.2, allows context-dependent attackers to cause a denial of service (resource exhaustion) via a regular expression containing adjacent repetition operators, as demonstrated by a {10,}{10,}{10,}{10,} sequence in the proftpd.gnu.c exploit for ProFTPD.", + "link": "https://security-tracker.debian.org/tracker/CVE-2010-4052", + "severity": "Negligible", + "fixedby": "" + }, + { + "featurename": "nettle", + "featureversion": "3.3-1", + "vulnerability": "CVE-2018-16869", + "namespace": "debian:9", + "description": "A Bleichenbacher type side-channel based padding oracle attack was found in the way nettle handles endian conversion of RSA decrypted PKCS#1 v1.5 data. An attacker who is able to run a process on the same physical core as the victim process, could use this flaw extract plaintext or in some cases downgrade any TLS connections to a vulnerable server.", + "link": "https://security-tracker.debian.org/tracker/CVE-2018-16869", + "severity": "Unknown", + "fixedby": "" + }, + { + "featurename": "perl", + "featureversion": "5.24.1-3+deb9u4", + "vulnerability": "CVE-2018-18311", + "namespace": "debian:9", + "description": "Perl before 5.26.3 and 5.28.x before 5.28.1 has a buffer overflow via a crafted regular expression that triggers invalid write operations.", + "link": "https://security-tracker.debian.org/tracker/CVE-2018-18311", + "severity": "Unknown", + "fixedby": "5.24.1-3+deb9u5" + } + ] +} diff --git a/spec/fixtures/sentry/issues_sample_response.json b/spec/fixtures/sentry/issues_sample_response.json new file mode 100644 index 00000000000..ed22499cfa1 --- /dev/null +++ b/spec/fixtures/sentry/issues_sample_response.json @@ -0,0 +1,42 @@ +[{ + "lastSeen": "2018-12-31T12:00:11Z", + "numComments": 0, + "userCount": 0, + "stats": { + "24h": [ + [ + 1546437600, + 0 + ] + ] + }, + "culprit": "sentry.tasks.reports.deliver_organization_user_report", + "title": "gaierror: [Errno -2] Name or service not known", + "id": "11", + "assignedTo": null, + "logger": null, + "type": "error", + "annotations": [], + "metadata": { + "type": "gaierror", + "value": "[Errno -2] Name or service not known" + }, + "status": "unresolved", + "subscriptionDetails": null, + "isPublic": false, + "hasSeen": false, + "shortId": "INTERNAL-4", + "shareId": null, + "firstSeen": "2018-12-17T12:00:14Z", + "count": "21", + "permalink": "35.228.54.90/sentry/internal/issues/11/", + "level": "error", + "isSubscribed": true, + "isBookmarked": false, + "project": { + "slug": "internal", + "id": "1", + "name": "Internal" + }, + "statusDetails": {} + }] diff --git a/spec/helpers/projects/error_tracking_helper_spec.rb b/spec/helpers/projects/error_tracking_helper_spec.rb new file mode 100644 index 00000000000..7516a636c93 --- /dev/null +++ b/spec/helpers/projects/error_tracking_helper_spec.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Projects::ErrorTrackingHelper do + include Gitlab::Routing.url_helpers + + set(:project) { create(:project) } + + describe '#error_tracking_data' do + let(:setting_path) { project_settings_operations_path(project) } + + let(:index_path) do + project_error_tracking_index_path(project, format: :json) + end + + context 'without error_tracking_setting' do + it 'returns frontend configuration' do + expect(error_tracking_data(project)).to eq( + 'index-path' => index_path, + 'enable-error-tracking-link' => setting_path, + 'error-tracking-enabled' => 'false', + "illustration-path" => "/images/illustrations/cluster_popover.svg" + ) + end + end + + context 'with error_tracking_setting' do + let(:error_tracking_setting) do + create(:project_error_tracking_setting, project: project) + end + + context 'when enabled' do + before do + error_tracking_setting.update!(enabled: true) + end + + it 'show error tracking enabled' do + expect(error_tracking_data(project)).to include( + 'error-tracking-enabled' => 'true' + ) + end + end + + context 'when disabled' do + before do + error_tracking_setting.update!(enabled: false) + end + + it 'show error tracking not enabled' do + expect(error_tracking_data(project)).to include( + 'error-tracking-enabled' => 'false' + ) + end + end + end + end +end diff --git a/spec/javascripts/diffs/components/compare_versions_dropdown_spec.js b/spec/javascripts/diffs/components/compare_versions_dropdown_spec.js index 7237274eb43..53b9ac22fc0 100644 --- a/spec/javascripts/diffs/components/compare_versions_dropdown_spec.js +++ b/spec/javascripts/diffs/components/compare_versions_dropdown_spec.js @@ -1 +1,34 @@ // TODO: https://gitlab.com/gitlab-org/gitlab-ce/issues/48034 +import { shallowMount, createLocalVue } from '@vue/test-utils'; +import CompareVersionsDropdown from '~/diffs/components/compare_versions_dropdown.vue'; +import diffsMockData from '../mock_data/merge_request_diffs'; + +describe('CompareVersionsDropdown', () => { + let wrapper; + const targetBranch = { branchName: 'tmp-wine-dev', versionIndex: -1 }; + + const factory = (options = {}) => { + const localVue = createLocalVue(); + + wrapper = shallowMount(CompareVersionsDropdown, { localVue, ...options }); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + it('should render a correct base version link', () => { + factory({ + propsData: { + baseVersionPath: '/gnuwget/wget2/merge_requests/6/diffs?diff_id=37', + otherVersions: diffsMockData.slice(1), + targetBranch, + }, + }); + + const links = wrapper.findAll('a'); + const lastLink = links.wrappers[links.length - 1]; + + expect(lastLink.attributes('href')).toEqual(wrapper.props('baseVersionPath')); + }); +}); diff --git a/spec/javascripts/diffs/components/compare_versions_spec.js b/spec/javascripts/diffs/components/compare_versions_spec.js index 75c66e9ca82..50135b0cf86 100644 --- a/spec/javascripts/diffs/components/compare_versions_spec.js +++ b/spec/javascripts/diffs/components/compare_versions_spec.js @@ -100,6 +100,12 @@ describe('CompareVersions', () => { }); }); + describe('baseVersionPath', () => { + it('should be set correctly from mergeRequestDiff', () => { + expect(vm.baseVersionPath).toEqual(vm.mergeRequestDiff.base_version_path); + }); + }); + describe('isWhitespaceVisible', () => { const originalHref = window.location.href; diff --git a/spec/javascripts/diffs/mock_data/merge_request_diffs.js b/spec/javascripts/diffs/mock_data/merge_request_diffs.js index d72ad7818dd..4bbef146336 100644 --- a/spec/javascripts/diffs/mock_data/merge_request_diffs.js +++ b/spec/javascripts/diffs/mock_data/merge_request_diffs.js @@ -1,42 +1,46 @@ export default [ { - versionIndex: 4, - createdAt: '2018-10-23T11:49:16.611Z', - commitsCount: 4, + base_version_path: '/gnuwget/wget2/merge_requests/6/diffs?diff_id=37', + version_index: 4, + created_at: '2018-10-23T11:49:16.611Z', + commits_count: 4, latest: true, - shortCommitSha: 'de7a8f7f', - versionPath: '/gnuwget/wget2/merge_requests/6/diffs?diff_id=37', - comparePath: + short_commit_sha: 'de7a8f7f', + version_path: '/gnuwget/wget2/merge_requests/6/diffs?diff_id=37', + compare_path: '/gnuwget/wget2/merge_requests/6/diffs?diff_id=37&start_sha=de7a8f7f20c3ea2e0bef3ba01cfd41c21f6b4995', }, { - versionIndex: 3, - createdAt: '2018-10-23T11:46:40.617Z', - commitsCount: 3, + base_version_path: '/gnuwget/wget2/merge_requests/6/diffs?diff_id=36', + version_index: 3, + created_at: '2018-10-23T11:46:40.617Z', + commits_count: 3, latest: false, - shortCommitSha: 'e78fc18f', - versionPath: '/gnuwget/wget2/merge_requests/6/diffs?diff_id=36', - comparePath: + short_commit_sha: 'e78fc18f', + version_path: '/gnuwget/wget2/merge_requests/6/diffs?diff_id=36', + compare_path: '/gnuwget/wget2/merge_requests/6/diffs?diff_id=37&start_sha=e78fc18fa37acb2185c59ca94d4a964464feb50e', }, { - versionIndex: 2, - createdAt: '2018-10-04T09:57:39.648Z', - commitsCount: 2, + base_version_path: '/gnuwget/wget2/merge_requests/6/diffs?diff_id=35', + version_index: 2, + created_at: '2018-10-04T09:57:39.648Z', + commits_count: 2, latest: false, - shortCommitSha: '48da7e7e', - versionPath: '/gnuwget/wget2/merge_requests/6/diffs?diff_id=35', - comparePath: + short_commit_sha: '48da7e7e', + version_path: '/gnuwget/wget2/merge_requests/6/diffs?diff_id=35', + compare_path: '/gnuwget/wget2/merge_requests/6/diffs?diff_id=37&start_sha=48da7e7e9a99d41c852578bd9cb541ca4d864b3e', }, { - versionIndex: 1, - createdAt: '2018-09-25T20:30:39.493Z', - commitsCount: 1, + base_version_path: '/gnuwget/wget2/merge_requests/6/diffs?diff_id=20', + version_index: 1, + created_at: '2018-09-25T20:30:39.493Z', + commits_count: 1, latest: false, - shortCommitSha: '47bac2ed', - versionPath: '/gnuwget/wget2/merge_requests/6/diffs?diff_id=20', - comparePath: + short_commit_sha: '47bac2ed', + version_path: '/gnuwget/wget2/merge_requests/6/diffs?diff_id=20', + compare_path: '/gnuwget/wget2/merge_requests/6/diffs?diff_id=37&start_sha=47bac2ed972c5bee344c1cea159a22cd7f711dc0', }, ]; diff --git a/spec/javascripts/error_tracking/components/error_tracking_list_spec.js b/spec/javascripts/error_tracking/components/error_tracking_list_spec.js new file mode 100644 index 00000000000..08bbb390993 --- /dev/null +++ b/spec/javascripts/error_tracking/components/error_tracking_list_spec.js @@ -0,0 +1,100 @@ +import { createLocalVue, shallowMount } from '@vue/test-utils'; +import Vuex from 'vuex'; +import ErrorTrackingList from '~/error_tracking/components/error_tracking_list.vue'; +import { GlButton, GlEmptyState, GlLoadingIcon, GlTable } from '@gitlab/ui'; + +const localVue = createLocalVue(); +localVue.use(Vuex); + +describe('ErrorTrackingList', () => { + let store; + let wrapper; + + function mountComponent({ errorTrackingEnabled = true } = {}) { + wrapper = shallowMount(ErrorTrackingList, { + localVue, + store, + propsData: { + indexPath: '/path', + enableErrorTrackingLink: '/link', + errorTrackingEnabled, + illustrationPath: 'illustration/path', + }, + }); + } + + beforeEach(() => { + const actions = { + getErrorList: () => {}, + }; + + const state = { + errors: [], + loading: true, + }; + + store = new Vuex.Store({ + actions, + state, + }); + }); + + afterEach(() => { + if (wrapper) { + wrapper.destroy(); + } + }); + + describe('loading', () => { + beforeEach(() => { + mountComponent(); + }); + + it('shows spinner', () => { + expect(wrapper.find(GlLoadingIcon).exists()).toBeTruthy(); + expect(wrapper.find(GlTable).exists()).toBeFalsy(); + expect(wrapper.find(GlButton).exists()).toBeFalsy(); + }); + }); + + describe('results', () => { + beforeEach(() => { + store.state.loading = false; + + mountComponent(); + }); + + it('shows table', () => { + expect(wrapper.find(GlLoadingIcon).exists()).toBeFalsy(); + expect(wrapper.find(GlTable).exists()).toBeTruthy(); + expect(wrapper.find(GlButton).exists()).toBeTruthy(); + }); + }); + + describe('no results', () => { + beforeEach(() => { + store.state.loading = false; + + mountComponent(); + }); + + it('shows empty table', () => { + expect(wrapper.find(GlLoadingIcon).exists()).toBeFalsy(); + expect(wrapper.find(GlTable).exists()).toBeTruthy(); + expect(wrapper.find(GlButton).exists()).toBeTruthy(); + }); + }); + + describe('error tracking feature disabled', () => { + beforeEach(() => { + mountComponent({ errorTrackingEnabled: false }); + }); + + it('shows empty state', () => { + expect(wrapper.find(GlEmptyState).exists()).toBeTruthy(); + expect(wrapper.find(GlLoadingIcon).exists()).toBeFalsy(); + expect(wrapper.find(GlTable).exists()).toBeFalsy(); + expect(wrapper.find(GlButton).exists()).toBeFalsy(); + }); + }); +}); diff --git a/spec/javascripts/error_tracking/store/mutation_spec.js b/spec/javascripts/error_tracking/store/mutation_spec.js new file mode 100644 index 00000000000..8117104bdbc --- /dev/null +++ b/spec/javascripts/error_tracking/store/mutation_spec.js @@ -0,0 +1,36 @@ +import mutations from '~/error_tracking/store/mutations'; +import * as types from '~/error_tracking/store/mutation_types'; + +describe('Error tracking mutations', () => { + describe('SET_ERRORS', () => { + let state; + + beforeEach(() => { + state = { errors: [] }; + }); + + it('camelizes response', () => { + const errors = [ + { + title: 'the title', + external_url: 'localhost:3456', + count: 100, + userCount: 10, + }, + ]; + + mutations[types.SET_ERRORS](state, errors); + + expect(state).toEqual({ + errors: [ + { + title: 'the title', + externalUrl: 'localhost:3456', + count: 100, + userCount: 10, + }, + ], + }); + }); + }); +}); diff --git a/spec/javascripts/fixtures/oauth_remember_me.html.haml b/spec/javascripts/fixtures/oauth_remember_me.html.haml index 7886e995e57..a5d7c4e816a 100644 --- a/spec/javascripts/fixtures/oauth_remember_me.html.haml +++ b/spec/javascripts/fixtures/oauth_remember_me.html.haml @@ -3,3 +3,4 @@ %a.oauth-login.twitter{ href: "http://example.com/" } %a.oauth-login.github{ href: "http://example.com/" } + %a.oauth-login.facebook{ href: "http://example.com/?redirect_fragment=L1" } diff --git a/spec/javascripts/fixtures/sessions.rb b/spec/javascripts/fixtures/sessions.rb new file mode 100644 index 00000000000..e90a58e8c54 --- /dev/null +++ b/spec/javascripts/fixtures/sessions.rb @@ -0,0 +1,26 @@ +require 'spec_helper' + +describe 'Sessions (JavaScript fixtures)' do + include JavaScriptFixturesHelpers + + before(:all) do + clean_frontend_fixtures('sessions/') + end + + describe SessionsController, '(JavaScript fixtures)', type: :controller do + include DeviseHelpers + + render_views + + before do + set_devise_mapping(context: @request) + end + + it 'sessions/new.html.raw' do |example| + get :new + + expect(response).to be_success + store_frontend_fixture(response, example.description) + end + end +end diff --git a/spec/javascripts/jobs/components/sidebar_spec.js b/spec/javascripts/jobs/components/sidebar_spec.js index b0bc16d7c64..3a02351460c 100644 --- a/spec/javascripts/jobs/components/sidebar_spec.js +++ b/spec/javascripts/jobs/components/sidebar_spec.js @@ -28,7 +28,7 @@ describe('Sidebar details block', () => { store, }); - expect(vm.$el.querySelector('.js-retry-job')).toBeNull(); + expect(vm.$el.querySelector('.js-retry-button')).toBeNull(); }); }); @@ -70,7 +70,7 @@ describe('Sidebar details block', () => { }); it('should render link to retry job', () => { - expect(vm.$el.querySelector('.js-retry-job').getAttribute('href')).toEqual(job.retry_path); + expect(vm.$el.querySelector('.js-retry-button').getAttribute('href')).toEqual(job.retry_path); }); it('should render link to cancel job', () => { diff --git a/spec/javascripts/jobs/store/getters_spec.js b/spec/javascripts/jobs/store/getters_spec.js index 4195d9d3680..7931b2af79f 100644 --- a/spec/javascripts/jobs/store/getters_spec.js +++ b/spec/javascripts/jobs/store/getters_spec.js @@ -8,30 +8,6 @@ describe('Job Store Getters', () => { localState = state(); }); - describe('headerActions', () => { - describe('with new issue path', () => { - it('returns an array with action to create a new issue', () => { - localState.job.new_issue_path = 'issues/new'; - - expect(getters.headerActions(localState)).toEqual([ - { - label: 'New issue', - path: localState.job.new_issue_path, - cssClass: - 'js-new-issue btn btn-success btn-inverted d-none d-md-block d-lg-block d-xl-block', - type: 'link', - }, - ]); - }); - }); - - describe('without new issue path', () => { - it('returns an empty array', () => { - expect(getters.headerActions(localState)).toEqual([]); - }); - }); - }); - describe('headerTime', () => { describe('when the job has started key', () => { it('returns started key', () => { diff --git a/spec/javascripts/lib/utils/common_utils_spec.js b/spec/javascripts/lib/utils/common_utils_spec.js index f320f232687..0dc7e93539a 100644 --- a/spec/javascripts/lib/utils/common_utils_spec.js +++ b/spec/javascripts/lib/utils/common_utils_spec.js @@ -347,20 +347,31 @@ describe('common_utils', () => { }); describe('parseBoolean', () => { + const { parseBoolean } = commonUtils; + it('returns true for "true"', () => { - expect(commonUtils.parseBoolean('true')).toEqual(true); + expect(parseBoolean('true')).toEqual(true); }); it('returns false for "false"', () => { - expect(commonUtils.parseBoolean('false')).toEqual(false); + expect(parseBoolean('false')).toEqual(false); }); it('returns false for "something"', () => { - expect(commonUtils.parseBoolean('something')).toEqual(false); + expect(parseBoolean('something')).toEqual(false); }); it('returns false for null', () => { - expect(commonUtils.parseBoolean(null)).toEqual(false); + expect(parseBoolean(null)).toEqual(false); + }); + + it('is idempotent', () => { + const input = ['true', 'false', 'something', null]; + input.forEach(value => { + const result = parseBoolean(value); + + expect(parseBoolean(result)).toBe(result); + }); }); }); diff --git a/spec/javascripts/lib/utils/url_utility_spec.js b/spec/javascripts/lib/utils/url_utility_spec.js index e4df8441793..381c7b2d0a6 100644 --- a/spec/javascripts/lib/utils/url_utility_spec.js +++ b/spec/javascripts/lib/utils/url_utility_spec.js @@ -1,4 +1,4 @@ -import { webIDEUrl, mergeUrlParams } from '~/lib/utils/url_utility'; +import * as urlUtils from '~/lib/utils/url_utility'; describe('URL utility', () => { describe('webIDEUrl', () => { @@ -8,7 +8,7 @@ describe('URL utility', () => { describe('without relative_url_root', () => { it('returns IDE path with route', () => { - expect(webIDEUrl('/gitlab-org/gitlab-ce/merge_requests/1')).toBe( + expect(urlUtils.webIDEUrl('/gitlab-org/gitlab-ce/merge_requests/1')).toBe( '/-/ide/project/gitlab-org/gitlab-ce/merge_requests/1', ); }); @@ -20,7 +20,7 @@ describe('URL utility', () => { }); it('returns IDE path with route', () => { - expect(webIDEUrl('/gitlab/gitlab-org/gitlab-ce/merge_requests/1')).toBe( + expect(urlUtils.webIDEUrl('/gitlab/gitlab-org/gitlab-ce/merge_requests/1')).toBe( '/gitlab/-/ide/project/gitlab-org/gitlab-ce/merge_requests/1', ); }); @@ -29,23 +29,82 @@ describe('URL utility', () => { describe('mergeUrlParams', () => { it('adds w', () => { - expect(mergeUrlParams({ w: 1 }, '#frag')).toBe('?w=1#frag'); - expect(mergeUrlParams({ w: 1 }, '/path#frag')).toBe('/path?w=1#frag'); - expect(mergeUrlParams({ w: 1 }, 'https://host/path')).toBe('https://host/path?w=1'); - expect(mergeUrlParams({ w: 1 }, 'https://host/path#frag')).toBe('https://host/path?w=1#frag'); - expect(mergeUrlParams({ w: 1 }, 'https://h/p?k1=v1#frag')).toBe('https://h/p?k1=v1&w=1#frag'); + expect(urlUtils.mergeUrlParams({ w: 1 }, '#frag')).toBe('?w=1#frag'); + expect(urlUtils.mergeUrlParams({ w: 1 }, '/path#frag')).toBe('/path?w=1#frag'); + expect(urlUtils.mergeUrlParams({ w: 1 }, 'https://host/path')).toBe('https://host/path?w=1'); + expect(urlUtils.mergeUrlParams({ w: 1 }, 'https://host/path#frag')).toBe( + 'https://host/path?w=1#frag', + ); + + expect(urlUtils.mergeUrlParams({ w: 1 }, 'https://h/p?k1=v1#frag')).toBe( + 'https://h/p?k1=v1&w=1#frag', + ); }); it('updates w', () => { - expect(mergeUrlParams({ w: 1 }, '?k1=v1&w=0#frag')).toBe('?k1=v1&w=1#frag'); + expect(urlUtils.mergeUrlParams({ w: 1 }, '?k1=v1&w=0#frag')).toBe('?k1=v1&w=1#frag'); }); it('adds multiple params', () => { - expect(mergeUrlParams({ a: 1, b: 2, c: 3 }, '#frag')).toBe('?a=1&b=2&c=3#frag'); + expect(urlUtils.mergeUrlParams({ a: 1, b: 2, c: 3 }, '#frag')).toBe('?a=1&b=2&c=3#frag'); }); it('adds and updates encoded params', () => { - expect(mergeUrlParams({ a: '&', q: '?' }, '?a=%23#frag')).toBe('?a=%26&q=%3F#frag'); + expect(urlUtils.mergeUrlParams({ a: '&', q: '?' }, '?a=%23#frag')).toBe('?a=%26&q=%3F#frag'); + }); + }); + + describe('removeParams', () => { + describe('when url is passed', () => { + it('removes query param with encoded ampersand', () => { + const url = urlUtils.removeParams(['filter'], '/mail?filter=n%3Djoe%26l%3Dhome'); + + expect(url).toBe('/mail'); + }); + + it('should remove param when url has no other params', () => { + const url = urlUtils.removeParams(['size'], '/feature/home?size=5'); + + expect(url).toBe('/feature/home'); + }); + + it('should remove param when url has other params', () => { + const url = urlUtils.removeParams(['size'], '/feature/home?q=1&size=5&f=html'); + + expect(url).toBe('/feature/home?q=1&f=html'); + }); + + it('should remove param and preserve fragment', () => { + const url = urlUtils.removeParams(['size'], '/feature/home?size=5#H2'); + + expect(url).toBe('/feature/home#H2'); + }); + + it('should remove multiple params', () => { + const url = urlUtils.removeParams(['z', 'a'], '/home?z=11111&l=en_US&a=true#H2'); + + expect(url).toBe('/home?l=en_US#H2'); + }); + }); + }); + + describe('setUrlFragment', () => { + it('should set fragment when url has no fragment', () => { + const url = urlUtils.setUrlFragment('/home/feature', 'usage'); + + expect(url).toBe('/home/feature#usage'); + }); + + it('should set fragment when url has existing fragment', () => { + const url = urlUtils.setUrlFragment('/home/feature#overview', 'usage'); + + expect(url).toBe('/home/feature#usage'); + }); + + it('should set fragment when given fragment includes #', () => { + const url = urlUtils.setUrlFragment('/home/feature#overview', '#install'); + + expect(url).toBe('/home/feature#install'); }); }); }); diff --git a/spec/javascripts/notebook/cells/output/html_spec.js b/spec/javascripts/notebook/cells/output/html_spec.js index bea62f54634..3ee404fb187 100644 --- a/spec/javascripts/notebook/cells/output/html_spec.js +++ b/spec/javascripts/notebook/cells/output/html_spec.js @@ -9,6 +9,8 @@ describe('html output cell', () => { return new Component({ propsData: { rawCode, + count: 0, + index: 0, }, }).$mount(); } diff --git a/spec/javascripts/notebook/cells/output/index_spec.js b/spec/javascripts/notebook/cells/output/index_spec.js index feab7ad4212..005569f1c2d 100644 --- a/spec/javascripts/notebook/cells/output/index_spec.js +++ b/spec/javascripts/notebook/cells/output/index_spec.js @@ -10,7 +10,7 @@ describe('Output component', () => { const createComponent = output => { vm = new Component({ propsData: { - output, + outputs: [].concat(output), count: 1, }, }); @@ -51,28 +51,21 @@ describe('Output component', () => { it('renders as an image', () => { expect(vm.$el.querySelector('img')).not.toBeNull(); }); - - it('does not render the prompt', () => { - expect(vm.$el.querySelector('.prompt span')).toBeNull(); - }); }); describe('html output', () => { - beforeEach(done => { + it('renders raw HTML', () => { createComponent(json.cells[4].outputs[0]); - setTimeout(() => { - done(); - }); - }); - - it('renders raw HTML', () => { expect(vm.$el.querySelector('p')).not.toBeNull(); - expect(vm.$el.textContent.trim()).toBe('test'); + expect(vm.$el.querySelectorAll('p').length).toBe(1); + expect(vm.$el.textContent.trim()).toContain('test'); }); - it('does not render the prompt', () => { - expect(vm.$el.querySelector('.prompt span')).toBeNull(); + it('renders multiple raw HTML outputs', () => { + createComponent([json.cells[4].outputs[0], json.cells[4].outputs[0]]); + + expect(vm.$el.querySelectorAll('p').length).toBe(2); }); }); @@ -88,10 +81,6 @@ describe('Output component', () => { it('renders as an svg', () => { expect(vm.$el.querySelector('svg')).not.toBeNull(); }); - - it('does not render the prompt', () => { - expect(vm.$el.querySelector('.prompt span')).toBeNull(); - }); }); describe('default to plain text', () => { diff --git a/spec/javascripts/notes/components/discussion_filter_spec.js b/spec/javascripts/notes/components/discussion_filter_spec.js index 5efcab436e4..91dab58ba7f 100644 --- a/spec/javascripts/notes/components/discussion_filter_spec.js +++ b/spec/javascripts/notes/components/discussion_filter_spec.js @@ -1,6 +1,7 @@ import Vue from 'vue'; import createStore from '~/notes/stores'; import DiscussionFilter from '~/notes/components/discussion_filter.vue'; +import { DISCUSSION_FILTERS_DEFAULT_VALUE } from '~/notes/constants'; import { mountComponentWithStore } from '../../helpers/vue_mount_component_helper'; import { discussionFiltersMock, discussionMock } from '../mock_data'; @@ -20,16 +21,14 @@ describe('DiscussionFilter component', () => { }, ]; const Component = Vue.extend(DiscussionFilter); - const selectedValue = discussionFiltersMock[0].value; + const selectedValue = DISCUSSION_FILTERS_DEFAULT_VALUE; + const props = { filters: discussionFiltersMock, selectedValue }; store.state.discussions = discussions; return mountComponentWithStore(Component, { el: null, store, - props: { - filters: discussionFiltersMock, - selectedValue, - }, + props, }); }; @@ -115,4 +114,41 @@ describe('DiscussionFilter component', () => { }); }); }); + + describe('URL with Links to notes', () => { + afterEach(() => { + window.location.hash = ''; + }); + + it('updates the filter when the URL links to a note', done => { + window.location.hash = `note_${discussionMock.notes[0].id}`; + vm.currentValue = discussionFiltersMock[2].value; + vm.handleLocationHash(); + + vm.$nextTick(() => { + expect(vm.currentValue).toEqual(DISCUSSION_FILTERS_DEFAULT_VALUE); + done(); + }); + }); + + it('does not update the filter when the current filter is "Show all activity"', done => { + window.location.hash = `note_${discussionMock.notes[0].id}`; + vm.handleLocationHash(); + + vm.$nextTick(() => { + expect(vm.currentValue).toEqual(DISCUSSION_FILTERS_DEFAULT_VALUE); + done(); + }); + }); + + it('only updates filter when the URL links to a note', done => { + window.location.hash = `testing123`; + vm.handleLocationHash(); + + vm.$nextTick(() => { + expect(vm.currentValue).toEqual(DISCUSSION_FILTERS_DEFAULT_VALUE); + done(); + }); + }); + }); }); diff --git a/spec/javascripts/oauth_remember_me_spec.js b/spec/javascripts/oauth_remember_me_spec.js index 2caa266b85f..4125706a407 100644 --- a/spec/javascripts/oauth_remember_me_spec.js +++ b/spec/javascripts/oauth_remember_me_spec.js @@ -20,6 +20,10 @@ describe('OAuthRememberMe', () => { expect($('#oauth-container .oauth-login.github').attr('href')).toBe( 'http://example.com/?remember_me=1', ); + + expect($('#oauth-container .oauth-login.facebook').attr('href')).toBe( + 'http://example.com/?redirect_fragment=L1&remember_me=1', + ); }); it('removes the "remember_me" query parameter from all OAuth login buttons', () => { @@ -28,5 +32,8 @@ describe('OAuthRememberMe', () => { expect($('#oauth-container .oauth-login.twitter').attr('href')).toBe('http://example.com/'); expect($('#oauth-container .oauth-login.github').attr('href')).toBe('http://example.com/'); + expect($('#oauth-container .oauth-login.facebook').attr('href')).toBe( + 'http://example.com/?redirect_fragment=L1', + ); }); }); diff --git a/spec/javascripts/pages/sessions/new/preserve_url_fragment_spec.js b/spec/javascripts/pages/sessions/new/preserve_url_fragment_spec.js new file mode 100644 index 00000000000..7a8227479d4 --- /dev/null +++ b/spec/javascripts/pages/sessions/new/preserve_url_fragment_spec.js @@ -0,0 +1,61 @@ +import $ from 'jquery'; +import preserveUrlFragment from '~/pages/sessions/new/preserve_url_fragment'; + +describe('preserve_url_fragment', () => { + preloadFixtures('sessions/new.html.raw'); + + beforeEach(() => { + loadFixtures('sessions/new.html.raw'); + }); + + it('adds the url fragment to all login and sign up form actions', () => { + preserveUrlFragment('#L65'); + + expect($('#new_user').attr('action')).toBe('http://test.host/users/sign_in#L65'); + expect($('#new_new_user').attr('action')).toBe('http://test.host/users#L65'); + }); + + it('does not add an empty url fragment to login and sign up form actions', () => { + preserveUrlFragment(); + + expect($('#new_user').attr('action')).toBe('http://test.host/users/sign_in'); + expect($('#new_new_user').attr('action')).toBe('http://test.host/users'); + }); + + it('does not add an empty query parameter to OmniAuth login buttons', () => { + preserveUrlFragment(); + + expect($('#oauth-login-cas3').attr('href')).toBe('http://test.host/users/auth/cas3'); + + expect($('.omniauth-container #oauth-login-auth0').attr('href')).toBe( + 'http://test.host/users/auth/auth0', + ); + }); + + describe('adds "redirect_fragment" query parameter to OmniAuth login buttons', () => { + it('when "remember_me" is not present', () => { + preserveUrlFragment('#L65'); + + expect($('#oauth-login-cas3').attr('href')).toBe( + 'http://test.host/users/auth/cas3?redirect_fragment=L65', + ); + + expect($('.omniauth-container #oauth-login-auth0').attr('href')).toBe( + 'http://test.host/users/auth/auth0?redirect_fragment=L65', + ); + }); + + it('when "remember-me" is present', () => { + $('a.omniauth-btn').attr('href', (i, href) => `${href}?remember_me=1`); + preserveUrlFragment('#L65'); + + expect($('#oauth-login-cas3').attr('href')).toBe( + 'http://test.host/users/auth/cas3?remember_me=1&redirect_fragment=L65', + ); + + expect($('#oauth-login-auth0').attr('href')).toBe( + 'http://test.host/users/auth/auth0?remember_me=1&redirect_fragment=L65', + ); + }); + }); +}); diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js index 2119a3b927a..e387367d1a2 100644 --- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js @@ -1,11 +1,12 @@ import Vue from 'vue'; import ReadyToMerge from '~/vue_merge_request_widget/components/states/ready_to_merge.vue'; +import SquashBeforeMerge from '~/vue_merge_request_widget/components/states/squash_before_merge.vue'; import eventHub from '~/vue_merge_request_widget/event_hub'; +import { createLocalVue, shallowMount } from '@vue/test-utils'; const commitMessage = 'This is the commit message'; const commitMessageWithDescription = 'This is the commit message description'; -const createComponent = (customConfig = {}) => { - const Component = Vue.extend(ReadyToMerge); +const createTestMr = customConfig => { const mr = { isPipelineActive: false, pipeline: null, @@ -16,6 +17,7 @@ const createComponent = (customConfig = {}) => { hasCI: false, ciStatus: null, sha: '12345678', + squash: false, commitMessage, commitMessageWithDescription, shouldRemoveSourceBranch: true, @@ -24,14 +26,23 @@ const createComponent = (customConfig = {}) => { Object.assign(mr, customConfig.mr); - const service = { - merge() {}, - poll() {}, - }; + return mr; +}; + +const createTestService = () => ({ + merge() {}, + poll() {}, +}); + +const createComponent = (customConfig = {}) => { + const Component = Vue.extend(ReadyToMerge); return new Component({ el: document.createElement('div'), - propsData: { mr, service }, + propsData: { + mr: createTestMr(customConfig), + service: createTestService(), + }, }); }; @@ -612,6 +623,47 @@ describe('ReadyToMerge', () => { }); }); + describe('Squash checkbox component', () => { + let wrapper; + const localVue = createLocalVue(); + + const createLocalComponent = (customConfig = {}) => { + wrapper = shallowMount(localVue.extend(ReadyToMerge), { + localVue, + propsData: { + mr: createTestMr(customConfig), + service: createTestService(), + }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + const findCheckboxElement = () => wrapper.find(SquashBeforeMerge); + + it('should be rendered when squash before merge is enabled and there is more than 1 commit', () => { + createLocalComponent({ + mr: { commitsCount: 2, enableSquashBeforeMerge: true }, + }); + + expect(findCheckboxElement().exists()).toBeTruthy(); + }); + + it('should not be rendered when squash before merge is disabled', () => { + createLocalComponent({ mr: { commitsCount: 2, enableSquashBeforeMerge: false } }); + + expect(findCheckboxElement().exists()).toBeFalsy(); + }); + + it('should not be rendered when there is only 1 commit', () => { + createLocalComponent({ mr: { commitsCount: 1, enableSquashBeforeMerge: true } }); + + expect(findCheckboxElement().exists()).toBeFalsy(); + }); + }); + describe('Merge controls', () => { describe('when allowed to merge', () => { beforeEach(() => { diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_squash_before_merge_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_squash_before_merge_spec.js new file mode 100644 index 00000000000..d6d8eecfcb9 --- /dev/null +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_squash_before_merge_spec.js @@ -0,0 +1,100 @@ +import { createLocalVue, shallowMount } from '@vue/test-utils'; +import SquashBeforeMerge from '~/vue_merge_request_widget/components/states/squash_before_merge.vue'; + +const localVue = createLocalVue(); + +describe('Squash before merge component', () => { + let wrapper; + + const createComponent = props => { + wrapper = shallowMount(localVue.extend(SquashBeforeMerge), { + localVue, + sync: false, + propsData: { + ...props, + }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + describe('checkbox', () => { + const findCheckbox = () => wrapper.find('.qa-squash-checkbox'); + + it('is unchecked if passed value prop is false', () => { + createComponent({ + value: false, + }); + + expect(findCheckbox().element.checked).toBeFalsy(); + }); + + it('is checked if passed value prop is true', () => { + createComponent({ + value: true, + }); + + expect(findCheckbox().element.checked).toBeTruthy(); + }); + + it('changes value on click', done => { + createComponent({ + value: false, + }); + + findCheckbox().element.checked = true; + + findCheckbox().trigger('change'); + + wrapper.vm.$nextTick(() => { + expect(findCheckbox().element.checked).toBeTruthy(); + done(); + }); + }); + + it('is disabled if isDisabled prop is true', () => { + createComponent({ + value: false, + isDisabled: true, + }); + + expect(findCheckbox().attributes('disabled')).toBeTruthy(); + }); + }); + + describe('about link', () => { + it('is not rendered if no help path is passed', () => { + createComponent({ + value: false, + }); + + const aboutLink = wrapper.find('a'); + + expect(aboutLink.exists()).toBeFalsy(); + }); + + it('is rendered if help path is passed', () => { + createComponent({ + value: false, + helpPath: 'test-path', + }); + + const aboutLink = wrapper.find('a'); + + expect(aboutLink.exists()).toBeTruthy(); + }); + + it('should have a correct help path if passed', () => { + createComponent({ + value: false, + helpPath: 'test-path', + }); + + const aboutLink = wrapper.find('a'); + + expect(aboutLink.attributes('href')).toEqual('test-path'); + }); + }); +}); diff --git a/spec/javascripts/vue_mr_widget/stores/get_state_key_spec.js b/spec/javascripts/vue_mr_widget/stores/get_state_key_spec.js index 61ef26cd080..b356ea85cad 100644 --- a/spec/javascripts/vue_mr_widget/stores/get_state_key_spec.js +++ b/spec/javascripts/vue_mr_widget/stores/get_state_key_spec.js @@ -76,4 +76,28 @@ describe('getStateKey', () => { expect(bound()).toEqual('archived'); }); + + it('returns rebased state key', () => { + const context = { + mergeStatus: 'checked', + mergeWhenPipelineSucceeds: false, + canMerge: true, + onlyAllowMergeIfPipelineSucceeds: true, + isPipelineFailed: true, + hasMergeableDiscussionsState: false, + isPipelineBlocked: false, + canBeMerged: false, + shouldBeRebased: true, + }; + const data = { + project_archived: false, + branch_missing: false, + commits_count: 2, + has_conflicts: false, + work_in_progress: false, + }; + const bound = getStateKey.bind(context, data); + + expect(bound()).toEqual('rebase'); + }); }); 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 index 8187b3204b1..12ee804f668 100644 --- a/spec/javascripts/vue_shared/components/markdown/suggestion_diff_header_spec.js +++ b/spec/javascripts/vue_shared/components/markdown/suggestion_diff_header_spec.js @@ -31,6 +31,12 @@ describe('Suggestion Diff component', () => { 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'); diff --git a/spec/javascripts/vue_shared/components/markdown/suggestion_diff_spec.js b/spec/javascripts/vue_shared/components/markdown/suggestion_diff_spec.js index d4ed8f2f7a4..f87c2a92f47 100644 --- a/spec/javascripts/vue_shared/components/markdown/suggestion_diff_spec.js +++ b/spec/javascripts/vue_shared/components/markdown/suggestion_diff_spec.js @@ -34,8 +34,8 @@ describe('Suggestion Diff component', () => { expect(vm.$el.querySelector('.qa-suggestion-diff-header')).not.toBeNull(); }); - it('renders a diff table', () => { - expect(vm.$el.querySelector('table.md-suggestion-diff')).not.toBeNull(); + it('renders a diff table with syntax highlighting', () => { + expect(vm.$el.querySelector('.md-suggestion-diff.js-syntax-highlight.code')).not.toBeNull(); }); it('renders the oldLineNumber', () => { diff --git a/spec/lib/api/helpers_spec.rb b/spec/lib/api/helpers_spec.rb index 1c73a936e17..e1aea82653d 100644 --- a/spec/lib/api/helpers_spec.rb +++ b/spec/lib/api/helpers_spec.rb @@ -150,32 +150,36 @@ describe API::Helpers do end describe '#send_git_blob' do - context 'content disposition' do - let(:repository) { double } - let(:blob) { double(name: 'foobar') } + let(:repository) { double } + let(:blob) { double(name: 'foobar') } - let(:send_git_blob) do - subject.send(:send_git_blob, repository, blob) - end + let(:send_git_blob) do + subject.send(:send_git_blob, repository, blob) + end - before do - allow(subject).to receive(:env).and_return({}) - allow(subject).to receive(:content_type) - allow(subject).to receive(:header).and_return({}) - allow(Gitlab::Workhorse).to receive(:send_git_blob) - end + before do + allow(subject).to receive(:env).and_return({}) + allow(subject).to receive(:content_type) + allow(subject).to receive(:header).and_return({}) + allow(Gitlab::Workhorse).to receive(:send_git_blob) + end + + it 'sets Gitlab::Workhorse::DETECT_HEADER header' do + expect(send_git_blob[Gitlab::Workhorse::DETECT_HEADER]).to eq "true" + end + context 'content disposition' do context 'when blob name is null' do let(:blob) { double(name: nil) } it 'returns only the disposition' do - expect(send_git_blob['Content-Disposition']).to eq 'attachment' + expect(send_git_blob['Content-Disposition']).to eq 'inline' end end context 'when blob name is not null' do it 'returns disposition with the blob name' do - expect(send_git_blob['Content-Disposition']).to eq 'attachment; filename="foobar"' + expect(send_git_blob['Content-Disposition']).to eq 'inline; filename="foobar"' end end end diff --git a/spec/lib/backup/manager_spec.rb b/spec/lib/backup/manager_spec.rb index 9633caac788..ae1c881e1f6 100644 --- a/spec/lib/backup/manager_spec.rb +++ b/spec/lib/backup/manager_spec.rb @@ -266,6 +266,7 @@ describe Backup::Manager do remote_directory: 'directory', multipart_chunk_size: 104857600, encryption: nil, + encryption_key: nil, storage_class: nil } ) diff --git a/spec/lib/bitbucket_server/client_spec.rb b/spec/lib/bitbucket_server/client_spec.rb index 5de0a9a65b5..b021e69800a 100644 --- a/spec/lib/bitbucket_server/client_spec.rb +++ b/spec/lib/bitbucket_server/client_spec.rb @@ -17,12 +17,6 @@ describe BitbucketServer::Client do subject.pull_requests(project, repo_slug) end - - it 'throws an exception when connection fails' do - allow(BitbucketServer::Collection).to receive(:new).and_raise(OpenSSL::SSL::SSLError) - - expect { subject.pull_requests(project, repo_slug) }.to raise_error(described_class::ServerError) - end end describe '#activities' do diff --git a/spec/lib/bitbucket_server/connection_spec.rb b/spec/lib/bitbucket_server/connection_spec.rb index b5da4cb1a49..ab8a5b89608 100644 --- a/spec/lib/bitbucket_server/connection_spec.rb +++ b/spec/lib/bitbucket_server/connection_spec.rb @@ -26,6 +26,12 @@ describe BitbucketServer::Connection do expect { subject.get(url) }.to raise_error(described_class::ConnectionError) end + + it 'throws an exception upon a network error' do + WebMock.stub_request(:get, url).with(headers: { 'Accept' => 'application/json' }).to_raise(OpenSSL::SSL::SSLError) + + expect { subject.get(url) }.to raise_error(described_class::ConnectionError) + end end describe '#post' do @@ -42,6 +48,12 @@ describe BitbucketServer::Connection do expect { subject.post(url, payload) }.to raise_error(described_class::ConnectionError) end + + it 'throws an exception upon a network error' do + WebMock.stub_request(:post, url).with(headers: { 'Accept' => 'application/json' }).to_raise(OpenSSL::SSL::SSLError) + + expect { subject.post(url, payload) }.to raise_error(described_class::ConnectionError) + end end describe '#delete' do @@ -63,6 +75,12 @@ describe BitbucketServer::Connection do expect { subject.delete(:branches, branch_path, payload) }.to raise_error(described_class::ConnectionError) end + + it 'throws an exception upon a network error' do + WebMock.stub_request(:delete, branch_url).with(headers: headers).to_raise(OpenSSL::SSL::SSLError) + + expect { subject.delete(:branches, branch_path, payload) }.to raise_error(described_class::ConnectionError) + end end end end diff --git a/spec/lib/feature_spec.rb b/spec/lib/feature_spec.rb index 9d56c62ae57..630732614b2 100644 --- a/spec/lib/feature_spec.rb +++ b/spec/lib/feature_spec.rb @@ -182,4 +182,18 @@ describe Feature do expect(described_class.disabled?(:enabled_feature_flag)).to be_falsey end end + + describe Feature::Target do + describe '#targets' do + let(:project) { create(:project) } + let(:user_name) { project.owner.username } + + subject { described_class.new(user: user_name, project: project.full_path) } + + it 'returns all found targets' do + expect(subject.targets).to be_an(Array) + expect(subject.targets).to eq([project.owner, project]) + end + end + end end diff --git a/spec/lib/gitlab/access/branch_protection_spec.rb b/spec/lib/gitlab/access/branch_protection_spec.rb new file mode 100644 index 00000000000..7f2979e8e28 --- /dev/null +++ b/spec/lib/gitlab/access/branch_protection_spec.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::Access::BranchProtection do + describe '#any?' do + using RSpec::Parameterized::TableSyntax + + where(:level, :result) do + Gitlab::Access::PROTECTION_NONE | false + Gitlab::Access::PROTECTION_DEV_CAN_PUSH | true + Gitlab::Access::PROTECTION_DEV_CAN_MERGE | true + Gitlab::Access::PROTECTION_FULL | true + end + + with_them do + it { expect(described_class.new(level).any?).to eq(result) } + end + end + + describe '#developer_can_push?' do + using RSpec::Parameterized::TableSyntax + + where(:level, :result) do + Gitlab::Access::PROTECTION_NONE | false + Gitlab::Access::PROTECTION_DEV_CAN_PUSH | true + Gitlab::Access::PROTECTION_DEV_CAN_MERGE | false + Gitlab::Access::PROTECTION_FULL | false + end + + with_them do + it do + expect(described_class.new(level).developer_can_push?).to eq(result) + end + end + end + + describe '#developer_can_merge?' do + using RSpec::Parameterized::TableSyntax + + where(:level, :result) do + Gitlab::Access::PROTECTION_NONE | false + Gitlab::Access::PROTECTION_DEV_CAN_PUSH | false + Gitlab::Access::PROTECTION_DEV_CAN_MERGE | true + Gitlab::Access::PROTECTION_FULL | false + end + + with_them do + it do + expect(described_class.new(level).developer_can_merge?).to eq(result) + end + end + end +end diff --git a/spec/lib/gitlab/auth/user_auth_finders_spec.rb b/spec/lib/gitlab/auth/user_auth_finders_spec.rb index 4e4c8b215c2..1e2aebdc84b 100644 --- a/spec/lib/gitlab/auth/user_auth_finders_spec.rb +++ b/spec/lib/gitlab/auth/user_auth_finders_spec.rb @@ -9,7 +9,7 @@ describe Gitlab::Auth::UserAuthFinders do 'rack.input' => '' } end - let(:request) { Rack::Request.new(env) } + let(:request) { ActionDispatch::Request.new(env) } def set_param(key, value) request.update_param(key, value) diff --git a/spec/lib/gitlab/background_migration/backfill_legacy_project_repositories_spec.rb b/spec/lib/gitlab/background_migration/backfill_legacy_project_repositories_spec.rb index ae4b53d62e6..947c99b860f 100644 --- a/spec/lib/gitlab/background_migration/backfill_legacy_project_repositories_spec.rb +++ b/spec/lib/gitlab/background_migration/backfill_legacy_project_repositories_spec.rb @@ -2,6 +2,6 @@ require 'spec_helper' -describe Gitlab::BackgroundMigration::BackfillLegacyProjectRepositories, :migration, schema: 20181218192239 do +describe Gitlab::BackgroundMigration::BackfillLegacyProjectRepositories, :migration, schema: 20181212171634 do it_behaves_like 'backfill migration for project repositories', :legacy end diff --git a/spec/lib/gitlab/background_migration/backfill_project_repositories_spec.rb b/spec/lib/gitlab/background_migration/backfill_project_repositories_spec.rb index 53c071f0268..510a0074554 100644 --- a/spec/lib/gitlab/background_migration/backfill_project_repositories_spec.rb +++ b/spec/lib/gitlab/background_migration/backfill_project_repositories_spec.rb @@ -2,6 +2,7 @@ require 'spec_helper' +# rubocop:disable RSpec/FactoriesInMigrationSpecs describe Gitlab::BackgroundMigration::BackfillProjectRepositories do let(:group) { create(:group, name: 'foo', path: 'foo') } @@ -34,6 +35,7 @@ describe Gitlab::BackgroundMigration::BackfillProjectRepositories do let!(:project_hashed_storage_2) { create(:project, name: 'bar', path: 'bar', namespace: group, storage_version: 2) } let!(:project_legacy_storage_3) { create(:project, name: 'baz', path: 'baz', namespace: group, storage_version: 0) } let!(:project_legacy_storage_4) { create(:project, name: 'zoo', path: 'zoo', namespace: group, storage_version: nil) } + let!(:project_legacy_storage_5) { create(:project, name: 'test', path: 'test', namespace: group, storage_version: nil) } describe '.on_hashed_storage' do it 'finds projects with repository on hashed storage' do @@ -47,7 +49,7 @@ describe Gitlab::BackgroundMigration::BackfillProjectRepositories do it 'finds projects with repository on legacy storage' do projects = described_class.on_legacy_storage.pluck(:id) - expect(projects).to match_array([project_legacy_storage_3.id, project_legacy_storage_4.id]) + expect(projects).to match_array([project_legacy_storage_3.id, project_legacy_storage_4.id, project_legacy_storage_5.id]) end end @@ -58,7 +60,7 @@ describe Gitlab::BackgroundMigration::BackfillProjectRepositories do projects = described_class.without_project_repository.pluck(:id) - expect(projects).to contain_exactly(project_hashed_storage_2.id, project_legacy_storage_4.id) + expect(projects).to contain_exactly(project_hashed_storage_2.id, project_legacy_storage_4.id, project_legacy_storage_5.id) end end @@ -78,17 +80,27 @@ describe Gitlab::BackgroundMigration::BackfillProjectRepositories do expect(project.disk_path).to eq(project_legacy_storage_3.disk_path) end + it 'returns the correct disk_path using the route entry' do + project_legacy_storage_5.route.update(path: 'zoo/new-test') + project = described_class.find(project_legacy_storage_5.id) + + expect(project.disk_path).to eq('zoo/new-test') + end + it 'raises OrphanedNamespaceError when any parent namespace does not exist' do subgroup = create(:group, parent: group) project_orphaned_namespace = create(:project, name: 'baz', path: 'baz', namespace: subgroup, storage_version: nil) subgroup.update_column(:parent_id, Namespace.maximum(:id).succ) project = described_class.find(project_orphaned_namespace.id) + project.route.destroy + subgroup.route.destroy - expect { project.disk_path } + expect { project.reload.disk_path } .to raise_error(Gitlab::BackgroundMigration::BackfillProjectRepositories::OrphanedNamespaceError) end end end end end +# rubocop:enable RSpec/FactoriesInMigrationSpecs diff --git a/spec/lib/gitlab/background_migration/create_gpg_key_subkeys_from_gpg_keys_spec.rb b/spec/lib/gitlab/background_migration/create_gpg_key_subkeys_from_gpg_keys_spec.rb index f92acf61682..f974dc8fda2 100644 --- a/spec/lib/gitlab/background_migration/create_gpg_key_subkeys_from_gpg_keys_spec.rb +++ b/spec/lib/gitlab/background_migration/create_gpg_key_subkeys_from_gpg_keys_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' describe Gitlab::BackgroundMigration::CreateGpgKeySubkeysFromGpgKeys, :migration, schema: 20171005130944 do context 'when GpgKey exists' do - let!(:gpg_key) { create(:gpg_key, key: GpgHelpers::User3.public_key) } + let!(:gpg_key) { create(:gpg_key, key: GpgHelpers::User3.public_key) } # rubocop:disable RSpec/FactoriesInMigrationSpecs before do GpgKeySubkey.destroy_all # rubocop: disable DestroyAll diff --git a/spec/lib/gitlab/background_migration/delete_diff_files_spec.rb b/spec/lib/gitlab/background_migration/delete_diff_files_spec.rb index 1969aed51da..27281333348 100644 --- a/spec/lib/gitlab/background_migration/delete_diff_files_spec.rb +++ b/spec/lib/gitlab/background_migration/delete_diff_files_spec.rb @@ -1,5 +1,6 @@ require 'spec_helper' +# rubocop:disable RSpec/FactoriesInMigrationSpecs describe Gitlab::BackgroundMigration::DeleteDiffFiles, :migration, :sidekiq, schema: 20180619121030 do describe '#perform' do context 'when diff files can be deleted' do @@ -71,3 +72,4 @@ describe Gitlab::BackgroundMigration::DeleteDiffFiles, :migration, :sidekiq, sch end end end +# rubocop:enable RSpec/FactoriesInMigrationSpecs diff --git a/spec/lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits_spec.rb b/spec/lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits_spec.rb index 5dce3fcbcb6..bc71a90605a 100644 --- a/spec/lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits_spec.rb +++ b/spec/lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits_spec.rb @@ -1,5 +1,6 @@ require 'spec_helper' +# rubocop:disable RSpec/FactoriesInMigrationSpecs describe Gitlab::BackgroundMigration::DeserializeMergeRequestDiffsAndCommits, :migration, schema: 20171114162227 do include GitHelpers @@ -324,3 +325,4 @@ describe Gitlab::BackgroundMigration::DeserializeMergeRequestDiffsAndCommits, :m end end end +# rubocop:enable RSpec/FactoriesInMigrationSpecs diff --git a/spec/lib/gitlab/background_migration/migrate_events_to_push_event_payloads_spec.rb b/spec/lib/gitlab/background_migration/migrate_events_to_push_event_payloads_spec.rb index 5432d270555..188969951a6 100644 --- a/spec/lib/gitlab/background_migration/migrate_events_to_push_event_payloads_spec.rb +++ b/spec/lib/gitlab/background_migration/migrate_events_to_push_event_payloads_spec.rb @@ -1,5 +1,6 @@ require 'spec_helper' +# rubocop:disable RSpec/FactoriesInMigrationSpecs describe Gitlab::BackgroundMigration::MigrateEventsToPushEventPayloads::Event, :migration, schema: 20170608152748 do describe '#commit_title' do it 'returns nil when there are no commits' do @@ -429,3 +430,4 @@ describe Gitlab::BackgroundMigration::MigrateEventsToPushEventPayloads, :migrati end end end +# rubocop:enable RSpec/FactoriesInMigrationSpecs diff --git a/spec/lib/gitlab/background_migration/migrate_system_uploads_to_new_folder_spec.rb b/spec/lib/gitlab/background_migration/migrate_system_uploads_to_new_folder_spec.rb index 021e1d14b18..ea8bdd48e72 100644 --- a/spec/lib/gitlab/background_migration/migrate_system_uploads_to_new_folder_spec.rb +++ b/spec/lib/gitlab/background_migration/migrate_system_uploads_to_new_folder_spec.rb @@ -1,5 +1,6 @@ require 'spec_helper' +# rubocop:disable RSpec/FactoriesInMigrationSpecs describe Gitlab::BackgroundMigration::MigrateSystemUploadsToNewFolder, :delete do let(:migration) { described_class.new } @@ -17,3 +18,4 @@ describe Gitlab::BackgroundMigration::MigrateSystemUploadsToNewFolder, :delete d end end end +# rubocop:enable RSpec/FactoriesInMigrationSpecs diff --git a/spec/lib/gitlab/background_migration/move_personal_snippet_files_spec.rb b/spec/lib/gitlab/background_migration/move_personal_snippet_files_spec.rb index 2e77e80ee46..593486fc56c 100644 --- a/spec/lib/gitlab/background_migration/move_personal_snippet_files_spec.rb +++ b/spec/lib/gitlab/background_migration/move_personal_snippet_files_spec.rb @@ -1,5 +1,6 @@ require 'spec_helper' +# rubocop:disable RSpec/FactoriesInMigrationSpecs describe Gitlab::BackgroundMigration::MovePersonalSnippetFiles do let(:test_dir) { File.join(Rails.root, 'tmp', 'tests', 'move_snippet_files_test') } let(:old_uploads_dir) { File.join('uploads', 'system', 'personal_snippet') } @@ -70,3 +71,4 @@ describe Gitlab::BackgroundMigration::MovePersonalSnippetFiles do "[an upload](#{path})" end end +# rubocop:enable RSpec/FactoriesInMigrationSpecs diff --git a/spec/lib/gitlab/background_migration/populate_cluster_kubernetes_namespace_table_spec.rb b/spec/lib/gitlab/background_migration/populate_cluster_kubernetes_namespace_table_spec.rb index 4f1b01eed41..8e3cb36d313 100644 --- a/spec/lib/gitlab/background_migration/populate_cluster_kubernetes_namespace_table_spec.rb +++ b/spec/lib/gitlab/background_migration/populate_cluster_kubernetes_namespace_table_spec.rb @@ -2,6 +2,7 @@ require 'spec_helper' +# rubocop:disable RSpec/FactoriesInMigrationSpecs describe Gitlab::BackgroundMigration::PopulateClusterKubernetesNamespaceTable, :migration, schema: 20181022173835 do let(:migration) { described_class.new } let(:clusters) { create_list(:cluster, 10, :project, :provided_by_gcp) } @@ -95,3 +96,4 @@ describe Gitlab::BackgroundMigration::PopulateClusterKubernetesNamespaceTable, : end end end +# rubocop:enable RSpec/FactoriesInMigrationSpecs diff --git a/spec/lib/gitlab/background_migration/populate_external_pipeline_source_spec.rb b/spec/lib/gitlab/background_migration/populate_external_pipeline_source_spec.rb index 6ab126ad39a..3e009fed0f1 100644 --- a/spec/lib/gitlab/background_migration/populate_external_pipeline_source_spec.rb +++ b/spec/lib/gitlab/background_migration/populate_external_pipeline_source_spec.rb @@ -2,6 +2,7 @@ require 'spec_helper' +# rubocop:disable RSpec/FactoriesInMigrationSpecs describe Gitlab::BackgroundMigration::PopulateExternalPipelineSource, :migration, schema: 20180916011959 do let(:migration) { described_class.new } @@ -65,3 +66,4 @@ describe Gitlab::BackgroundMigration::PopulateExternalPipelineSource, :migration it_behaves_like 'no changes' end end +# rubocop:enable RSpec/FactoriesInMigrationSpecs diff --git a/spec/lib/gitlab/background_migration/populate_merge_request_metrics_with_events_data_spec.rb b/spec/lib/gitlab/background_migration/populate_merge_request_metrics_with_events_data_spec.rb index e99257e3481..ff1bd9f7850 100644 --- a/spec/lib/gitlab/background_migration/populate_merge_request_metrics_with_events_data_spec.rb +++ b/spec/lib/gitlab/background_migration/populate_merge_request_metrics_with_events_data_spec.rb @@ -1,5 +1,6 @@ require 'rails_helper' +# rubocop:disable RSpec/FactoriesInMigrationSpecs describe Gitlab::BackgroundMigration::PopulateMergeRequestMetricsWithEventsData, :migration, schema: 20171128214150 do # commits_count attribute is added in a next migration before do @@ -128,3 +129,4 @@ describe Gitlab::BackgroundMigration::PopulateMergeRequestMetricsWithEventsData, end end end +# rubocop:enable RSpec/FactoriesInMigrationSpecs diff --git a/spec/lib/gitlab/ci/config/entry/global_spec.rb b/spec/lib/gitlab/ci/config/entry/global_spec.rb index 941ef33c8a4..7651f594a4c 100644 --- a/spec/lib/gitlab/ci/config/entry/global_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/global_spec.rb @@ -160,8 +160,7 @@ describe Gitlab::Ci::Config::Entry::Global do variables: { 'VAR' => 'value' }, ignore: false, after_script: ['make clean'], - only: { refs: %w[branches tags] }, - except: {} }, + only: { refs: %w[branches tags] } }, spinach: { name: :spinach, before_script: [], script: %w[spinach], @@ -172,8 +171,7 @@ describe Gitlab::Ci::Config::Entry::Global do variables: {}, ignore: false, after_script: ['make clean'], - only: { refs: %w[branches tags] }, - except: {} } + only: { refs: %w[branches tags] } } ) end end diff --git a/spec/lib/gitlab/ci/config/entry/job_spec.rb b/spec/lib/gitlab/ci/config/entry/job_spec.rb index 3d0b98eb238..0560eb42e4d 100644 --- a/spec/lib/gitlab/ci/config/entry/job_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/job_spec.rb @@ -258,8 +258,7 @@ describe Gitlab::Ci::Config::Entry::Job do stage: 'test', ignore: false, after_script: %w[cleanup], - only: { refs: %w[branches tags] }, - except: {}) + only: { refs: %w[branches tags] }) end end end diff --git a/spec/lib/gitlab/ci/config/entry/jobs_spec.rb b/spec/lib/gitlab/ci/config/entry/jobs_spec.rb index d97be76f0e0..271ee30df3c 100644 --- a/spec/lib/gitlab/ci/config/entry/jobs_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/jobs_spec.rb @@ -67,14 +67,12 @@ describe Gitlab::Ci::Config::Entry::Jobs do script: %w[rspec], ignore: false, stage: 'test', - only: { refs: %w[branches tags] }, - except: {} }, + only: { refs: %w[branches tags] } }, spinach: { name: :spinach, script: %w[spinach], ignore: false, stage: 'test', - only: { refs: %w[branches tags] }, - except: {} }) + only: { refs: %w[branches tags] } }) end end diff --git a/spec/lib/gitlab/ci/config/entry/policy_spec.rb b/spec/lib/gitlab/ci/config/entry/policy_spec.rb index 83001b7fdd8..1c987e13a9a 100644 --- a/spec/lib/gitlab/ci/config/entry/policy_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/policy_spec.rb @@ -168,8 +168,33 @@ describe Gitlab::Ci::Config::Entry::Policy do end end + describe '#value' do + context 'when default value has been provided' do + before do + entry.default = { refs: %w[branches tags] } + end + + context 'when user overrides default values' do + let(:config) { { refs: %w[feature], variables: %w[$VARIABLE] } } + + it 'does not include default values' do + expect(entry.value).to eq config + end + end + + context 'when default value has not been defined' do + let(:config) { { variables: %w[$VARIABLE] } } + + it 'includes default values' do + expect(entry.value).to eq(refs: %w[branches tags], + variables: %w[$VARIABLE]) + end + end + end + end + describe '.default' do - it 'does not have a default value' do + it 'does not have default policy' do expect(described_class.default).to be_nil end end diff --git a/spec/lib/gitlab/ci/config/normalizer_spec.rb b/spec/lib/gitlab/ci/config/normalizer_spec.rb index 97926695b6e..cd880177170 100644 --- a/spec/lib/gitlab/ci/config/normalizer_spec.rb +++ b/spec/lib/gitlab/ci/config/normalizer_spec.rb @@ -62,5 +62,25 @@ describe Gitlab::Ci::Config::Normalizer do expect(subject[:other_job][:dependencies]).not_to include(job_name) end end + + context 'when there are dependencies which are both parallelized and not' do + let(:config) do + { + job_name => job_config, + other_job: { script: 'echo 1' }, + final_job: { script: 'echo 1', dependencies: [job_name.to_s, "other_job"] } + } + end + + it 'parallelizes dependencies' do + job_names = ["rspec 1/5", "rspec 2/5", "rspec 3/5", "rspec 4/5", "rspec 5/5"] + + expect(subject[:final_job][:dependencies]).to include(*job_names) + end + + it 'includes the regular job in dependencies' do + expect(subject[:final_job][:dependencies]).to include('other_job') + end + end end end diff --git a/spec/lib/gitlab/config/entry/configurable_spec.rb b/spec/lib/gitlab/config/entry/configurable_spec.rb index 85a7cf1d241..37e38e49c0d 100644 --- a/spec/lib/gitlab/config/entry/configurable_spec.rb +++ b/spec/lib/gitlab/config/entry/configurable_spec.rb @@ -7,6 +7,10 @@ describe Gitlab::Config::Entry::Configurable do end end + before do + allow(entry).to receive(:default) + end + describe 'validations' do context 'when entry is a hash' do let(:instance) { entry.new(key: 'value') } @@ -26,9 +30,11 @@ describe Gitlab::Config::Entry::Configurable do end describe 'configured entries' do + let(:entry_class) { double('entry_class', default: nil) } + before do - entry.class_eval do - entry :object, Object, description: 'test object' + entry.class_exec(entry_class) do |entry_class| + entry :object, entry_class, description: 'test object' end end diff --git a/spec/lib/gitlab/git/bundle_file_spec.rb b/spec/lib/gitlab/git/bundle_file_spec.rb new file mode 100644 index 00000000000..ff7c981dadd --- /dev/null +++ b/spec/lib/gitlab/git/bundle_file_spec.rb @@ -0,0 +1,26 @@ +require 'spec_helper' + +describe Gitlab::Git::BundleFile do + describe '.check!' do + let(:valid_bundle) { Tempfile.new } + let(:valid_bundle_path) { valid_bundle.path } + let(:invalid_bundle_path) { Rails.root.join('spec/fixtures/malicious.bundle') } + + after do + valid_bundle.close! + end + + it 'returns nil for a valid bundle' do + valid_bundle.write("# v2 git bundle\nfoo bar baz\n") + valid_bundle.close + + expect(described_class.check!(valid_bundle_path)).to be_nil + end + + it 'raises an exception for an invalid bundle' do + expect do + described_class.check!(invalid_bundle_path) + end.to raise_error(described_class::InvalidBundleError) + end + end +end diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index 852ee9c96af..a19e3e84f83 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -1753,22 +1753,23 @@ describe Gitlab::Git::Repository, :seed_helper do end describe '#create_from_bundle' do - let(:bundle_path) { File.join(Dir.tmpdir, "repo-#{SecureRandom.hex}.bundle") } + let(:valid_bundle_path) { File.join(Dir.tmpdir, "repo-#{SecureRandom.hex}.bundle") } + let(:malicious_bundle_path) { Rails.root.join('spec/fixtures/malicious.bundle') } let(:project) { create(:project) } let(:imported_repo) { project.repository.raw } before do - expect(repository.bundle_to_disk(bundle_path)).to be_truthy + expect(repository.bundle_to_disk(valid_bundle_path)).to be_truthy end after do - FileUtils.rm_rf(bundle_path) + FileUtils.rm_rf(valid_bundle_path) end it 'creates a repo from a bundle file' do expect(imported_repo).not_to exist - result = imported_repo.create_from_bundle(bundle_path) + result = imported_repo.create_from_bundle(valid_bundle_path) expect(result).to be_truthy expect(imported_repo).to exist @@ -1776,11 +1777,17 @@ describe Gitlab::Git::Repository, :seed_helper do end it 'creates a symlink to the global hooks dir' do - imported_repo.create_from_bundle(bundle_path) + imported_repo.create_from_bundle(valid_bundle_path) hooks_path = Gitlab::GitalyClient::StorageSettings.allow_disk_access { File.join(imported_repo.path, 'hooks') } expect(File.readlink(hooks_path)).to eq(Gitlab.config.gitlab_shell.hooks_path) end + + it 'raises an error if the bundle is an attempted malicious payload' do + expect do + imported_repo.create_from_bundle(malicious_bundle_path) + end.to raise_error(::Gitlab::Git::BundleFile::InvalidBundleError) + end end describe '#checksum' do diff --git a/spec/lib/gitlab/gon_helper_spec.rb b/spec/lib/gitlab/gon_helper_spec.rb index c6f09ca2112..1ff2334bacf 100644 --- a/spec/lib/gitlab/gon_helper_spec.rb +++ b/spec/lib/gitlab/gon_helper_spec.rb @@ -29,4 +29,13 @@ describe Gitlab::GonHelper do helper.push_frontend_feature_flag(:my_feature_flag, 10) end end + + describe '#default_avatar_url' do + it 'returns an absolute URL' do + url = helper.default_avatar_url + + expect(url).to match(/^http/) + expect(url).to match(/no_avatar.*png$/) + end + end end diff --git a/spec/lib/gitlab/middleware/read_only_spec.rb b/spec/lib/gitlab/middleware/read_only_spec.rb index bdb1f34d2f6..24d49a049b6 100644 --- a/spec/lib/gitlab/middleware/read_only_spec.rb +++ b/spec/lib/gitlab/middleware/read_only_spec.rb @@ -101,16 +101,36 @@ describe Gitlab::Middleware::ReadOnly do expect(subject).not_to disallow_request end - it 'expects requests to sidekiq admin to be allowed' do - response = request.post('/admin/sidekiq') + context 'sidekiq admin requests' do + where(:mounted_at) do + [ + '', + '/', + '/gitlab', + '/gitlab/', + '/gitlab/gitlab', + '/gitlab/gitlab/' + ] + end - expect(response).not_to be_redirect - expect(subject).not_to disallow_request + with_them do + before do + stub_config_setting(relative_url_root: mounted_at) + end - response = request.get('/admin/sidekiq') + it 'allows requests' do + path = File.join(mounted_at, 'admin/sidekiq') + response = request.post(path) - expect(response).not_to be_redirect - expect(subject).not_to disallow_request + expect(response).not_to be_redirect + expect(subject).not_to disallow_request + + response = request.get(path) + + expect(response).not_to be_redirect + expect(subject).not_to disallow_request + end + end end where(:description, :path) do diff --git a/spec/lib/gitlab/release_blog_post_spec.rb b/spec/lib/gitlab/release_blog_post_spec.rb new file mode 100644 index 00000000000..2c987df3767 --- /dev/null +++ b/spec/lib/gitlab/release_blog_post_spec.rb @@ -0,0 +1,97 @@ +require 'spec_helper' + +describe Gitlab::ReleaseBlogPost do + describe '.blog_post_url' do + let(:releases_xml) do + <<~EOS + <?xml version='1.0' encoding='utf-8' ?> + <feed xmlns='http://www.w3.org/2005/Atom'> + <entry> + <release>11.2</release> + <id>https://about.gitlab.com/2018/08/22/gitlab-11-2-released/</id> + </entry> + <entry> + <release>11.1</release> + <id>https://about.gitlab.com/2018/07/22/gitlab-11-1-released/</id> + </entry> + <entry> + <release>11.0</release> + <id>https://about.gitlab.com/2018/06/22/gitlab-11-0-released/</id> + </entry> + <entry> + <release>10.8</release> + <id>https://about.gitlab.com/2018/05/22/gitlab-10-8-released/</id> + </entry> + </feed> + EOS + end + + subject { described_class.send(:new).blog_post_url } + + before do + stub_request(:get, 'https://about.gitlab.com/releases.xml') + .to_return(status: 200, headers: { 'content-type' => ['text/xml'] }, body: releases_xml) + end + + context 'matches GitLab version to blog post url' do + it 'returns the correct url for major pre release' do + stub_const('Gitlab::VERSION', '11.0.0-pre') + + expect(subject).to eql('https://about.gitlab.com/2018/05/22/gitlab-10-8-released/') + end + + it 'returns the correct url for major release candidate' do + stub_const('Gitlab::VERSION', '11.0.0-rc3') + + expect(subject).to eql('https://about.gitlab.com/2018/05/22/gitlab-10-8-released/') + end + + it 'returns the correct url for major release' do + stub_const('Gitlab::VERSION', '11.0.0') + + expect(subject).to eql('https://about.gitlab.com/2018/06/22/gitlab-11-0-released/') + end + + it 'returns the correct url for minor pre release' do + stub_const('Gitlab::VERSION', '11.2.0-pre') + + expect(subject).to eql('https://about.gitlab.com/2018/07/22/gitlab-11-1-released/') + end + + it 'returns the correct url for minor release candidate' do + stub_const('Gitlab::VERSION', '11.2.0-rc3') + + expect(subject).to eql('https://about.gitlab.com/2018/07/22/gitlab-11-1-released/') + end + + it 'returns the correct url for minor release' do + stub_const('Gitlab::VERSION', '11.2.0') + + expect(subject).to eql('https://about.gitlab.com/2018/08/22/gitlab-11-2-released/') + end + + it 'returns the correct url for patch pre release' do + stub_const('Gitlab::VERSION', '11.2.1-pre') + expect(subject).to eql('https://about.gitlab.com/2018/08/22/gitlab-11-2-released/') + end + + it 'returns the correct url for patch release candidate' do + stub_const('Gitlab::VERSION', '11.2.1-rc3') + + expect(subject).to eql('https://about.gitlab.com/2018/08/22/gitlab-11-2-released/') + end + + it 'returns the correct url for patch release' do + stub_const('Gitlab::VERSION', '11.2.1') + + expect(subject).to eql('https://about.gitlab.com/2018/08/22/gitlab-11-2-released/') + end + + it 'returns nil when no blog post is matched' do + stub_const('Gitlab::VERSION', '9.0.0') + + expect(subject).to be(nil) + end + end + end +end diff --git a/spec/lib/gitlab/repository_cache_spec.rb b/spec/lib/gitlab/repository_cache_spec.rb index 1b9a8b4ab0d..741ee12633f 100644 --- a/spec/lib/gitlab/repository_cache_spec.rb +++ b/spec/lib/gitlab/repository_cache_spec.rb @@ -4,14 +4,14 @@ describe Gitlab::RepositoryCache do let(:backend) { double('backend').as_null_object } let(:project) { create(:project) } let(:repository) { project.repository } - let(:namespace) { "project:#{project.id}" } + let(:namespace) { "#{repository.full_path}:#{project.id}" } let(:cache) { described_class.new(repository, backend: backend) } describe '#cache_key' do subject { cache.cache_key(:foo) } it 'includes the namespace' do - expect(subject).to eq "#{namespace}:foo" + expect(subject).to eq "foo:#{namespace}" end context 'with a given namespace' do @@ -22,7 +22,7 @@ describe Gitlab::RepositoryCache do end it 'includes the full namespace' do - expect(subject).to eq "#{namespace}:#{extra_namespace}:foo" + expect(subject).to eq "foo:#{namespace}:#{extra_namespace}" end end end @@ -30,21 +30,21 @@ describe Gitlab::RepositoryCache do describe '#expire' do it 'expires the given key from the cache' do cache.expire(:foo) - expect(backend).to have_received(:delete).with("#{namespace}:foo") + expect(backend).to have_received(:delete).with("foo:#{namespace}") end end describe '#fetch' do it 'fetches the given key from the cache' do cache.fetch(:bar) - expect(backend).to have_received(:fetch).with("#{namespace}:bar") + expect(backend).to have_received(:fetch).with("bar:#{namespace}") end it 'accepts a block' do p = -> {} cache.fetch(:baz, &p) - expect(backend).to have_received(:fetch).with("#{namespace}:baz", &p) + expect(backend).to have_received(:fetch).with("baz:#{namespace}", &p) end end @@ -67,7 +67,7 @@ describe Gitlab::RepositoryCache do end it 'caches the value' do - expect(backend).to receive(:write).with("#{namespace}:#{key}", true) + expect(backend).to receive(:write).with("#{key}:#{namespace}", true) cache.fetch_without_caching_false(key) { true } end @@ -83,7 +83,7 @@ describe Gitlab::RepositoryCache do end it 'does not cache the value' do - expect(backend).not_to receive(:write).with("#{namespace}:#{key}", true) + expect(backend).not_to receive(:write).with("#{key}:#{namespace}", true) cache.fetch_without_caching_false(key, &p) end @@ -92,7 +92,7 @@ describe Gitlab::RepositoryCache do context 'when the cached value is truthy' do before do - backend.write("#{namespace}:#{key}", true) + backend.write("#{key}:#{namespace}", true) end it 'returns the cached value' do @@ -116,7 +116,7 @@ describe Gitlab::RepositoryCache do context 'when the cached value is falsey' do before do - backend.write("#{namespace}:#{key}", false) + backend.write("#{key}:#{namespace}", false) end it 'returns the result of the block' do @@ -126,7 +126,7 @@ describe Gitlab::RepositoryCache do end it 'writes the truthy value to the cache' do - expect(backend).to receive(:write).with("#{namespace}:#{key}", 'block result') + expect(backend).to receive(:write).with("#{key}:#{namespace}", 'block result') cache.fetch_without_caching_false(key) { 'block result' } end diff --git a/spec/lib/gitlab/request_context_spec.rb b/spec/lib/gitlab/request_context_spec.rb index 8a28ad0e597..fd443cc1f71 100644 --- a/spec/lib/gitlab/request_context_spec.rb +++ b/spec/lib/gitlab/request_context_spec.rb @@ -15,7 +15,7 @@ describe Gitlab::RequestContext do let(:ip) { '192.168.1.11' } before do - allow_any_instance_of(Rack::Request).to receive(:ip).and_return(ip) + allow_any_instance_of(ActionDispatch::Request).to receive(:ip).and_return(ip) described_class.new(app).call(env) end diff --git a/spec/lib/gitlab/tracing/factory_spec.rb b/spec/lib/gitlab/tracing/factory_spec.rb new file mode 100644 index 00000000000..945490f0988 --- /dev/null +++ b/spec/lib/gitlab/tracing/factory_spec.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +require 'fast_spec_helper' + +describe Gitlab::Tracing::Factory do + describe '.create_tracer' do + let(:service_name) { 'rspec' } + + context "when tracing is not configured" do + it 'ignores null connection strings' do + expect(described_class.create_tracer(service_name, nil)).to be_nil + end + + it 'ignores empty connection strings' do + expect(described_class.create_tracer(service_name, '')).to be_nil + end + + it 'ignores unknown implementations' do + expect(described_class.create_tracer(service_name, 'opentracing://invalid_driver')).to be_nil + end + + it 'ignores invalid connection strings' do + expect(described_class.create_tracer(service_name, 'open?tracing')).to be_nil + end + end + + context "when tracing is configured with jaeger" do + let(:mock_tracer) { double('tracer') } + + it 'processes default connections' do + expect(Gitlab::Tracing::JaegerFactory).to receive(:create_tracer).with(service_name, {}).and_return(mock_tracer) + + expect(described_class.create_tracer(service_name, 'opentracing://jaeger')).to be(mock_tracer) + end + + it 'processes connections with parameters' do + expect(Gitlab::Tracing::JaegerFactory).to receive(:create_tracer).with(service_name, { a: '1', b: '2', c: '3' }).and_return(mock_tracer) + + expect(described_class.create_tracer(service_name, 'opentracing://jaeger?a=1&b=2&c=3')).to be(mock_tracer) + end + end + end +end diff --git a/spec/lib/gitlab/tracing/jaeger_factory_spec.rb b/spec/lib/gitlab/tracing/jaeger_factory_spec.rb new file mode 100644 index 00000000000..3bffeb28830 --- /dev/null +++ b/spec/lib/gitlab/tracing/jaeger_factory_spec.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +require 'fast_spec_helper' + +describe Gitlab::Tracing::JaegerFactory do + describe '.create_tracer' do + let(:service_name) { 'rspec' } + + it 'processes default connections' do + expect(described_class.create_tracer(service_name, {})).to respond_to(:active_span) + end + + it 'handles debug options' do + expect(described_class.create_tracer(service_name, { debug: "1" })).to respond_to(:active_span) + end + + it 'handles const sampler' do + expect(described_class.create_tracer(service_name, { sampler: "const", sampler_param: "1" })).to respond_to(:active_span) + end + + it 'handles probabilistic sampler' do + expect(described_class.create_tracer(service_name, { sampler: "probabilistic", sampler_param: "0.5" })).to respond_to(:active_span) + end + + it 'handles http_endpoint configurations' do + expect(described_class.create_tracer(service_name, { http_endpoint: "http://localhost:1234" })).to respond_to(:active_span) + end + + it 'handles udp_endpoint configurations' do + expect(described_class.create_tracer(service_name, { udp_endpoint: "localhost:4321" })).to respond_to(:active_span) + end + + it 'ignores invalid parameters' do + expect(described_class.create_tracer(service_name, { invalid: "true" })).to respond_to(:active_span) + end + + it 'accepts the debug parameter when strict_parser is set' do + expect(described_class.create_tracer(service_name, { debug: "1", strict_parsing: "1" })).to respond_to(:active_span) + end + + it 'rejects invalid parameters when strict_parser is set' do + expect { described_class.create_tracer(service_name, { invalid: "true", strict_parsing: "1" }) }.to raise_error(StandardError) + end + end +end diff --git a/spec/lib/gitlab_spec.rb b/spec/lib/gitlab_spec.rb index d63f448883b..6ac3d115bc6 100644 --- a/spec/lib/gitlab_spec.rb +++ b/spec/lib/gitlab_spec.rb @@ -8,6 +8,7 @@ describe Gitlab do expect(described_class.root).to eq(Pathname.new(File.expand_path('../..', __dir__))) end end + describe '.revision' do let(:cmd) { %W[#{described_class.config.git.bin_path} log --pretty=format:%h -n 1] } @@ -69,6 +70,82 @@ describe Gitlab do end end + describe '.final_release?' do + subject { described_class.final_release? } + + context 'returns the corrent boolean value' do + it 'is false for a pre release' do + stub_const('Gitlab::VERSION', '11.0.0-pre') + + expect(subject).to be false + end + + it 'is false for a release candidate' do + stub_const('Gitlab::VERSION', '11.0.0-rc2') + + expect(subject).to be false + end + + it 'is true for a final release' do + stub_const('Gitlab::VERSION', '11.0.2') + + expect(subject).to be true + end + end + end + + describe '.minor_release' do + subject { described_class.minor_release } + + it 'returns the minor release of the full GitLab version' do + stub_const('Gitlab::VERSION', '11.0.1-rc3') + + expect(subject).to eql '11.0' + end + end + + describe '.previous_release' do + subject { described_class.previous_release } + + context 'it should return the previous release' do + it 'returns the previous major version when GitLab major version is not final' do + stub_const('Gitlab::VERSION', '11.0.1-pre') + + expect(subject).to eql '10' + end + + it 'returns the current minor version when the GitLab patch version is RC and > 0' do + stub_const('Gitlab::VERSION', '11.2.1-rc3') + + expect(subject).to eql '11.2' + end + + it 'returns the previous minor version when the GitLab patch version is RC and 0' do + stub_const('Gitlab::VERSION', '11.2.0-rc3') + + expect(subject).to eql '11.1' + end + end + end + + describe '.new_major_release?' do + subject { described_class.new_major_release? } + + context 'returns the corrent boolean value' do + it 'is true when the minor version is 0 and the patch is a pre release' do + stub_const('Gitlab::VERSION', '11.0.1-pre') + + expect(subject).to be true + end + + it 'is false when the minor version is above 0' do + stub_const('Gitlab::VERSION', '11.2.1-rc3') + + expect(subject).to be false + end + end + end + describe '.com?' do it 'is true when on GitLab.com' do stub_config_setting(url: 'https://gitlab.com') diff --git a/spec/lib/omni_auth/strategies/jwt_spec.rb b/spec/lib/omni_auth/strategies/jwt_spec.rb index c2e2db27362..c1eaf0bb0bf 100644 --- a/spec/lib/omni_auth/strategies/jwt_spec.rb +++ b/spec/lib/omni_auth/strategies/jwt_spec.rb @@ -25,6 +25,8 @@ describe OmniAuth::Strategies::Jwt do subject.options[:secret] = secret subject.options[:algorithm] = algorithm + # We use Rack::Request instead of ActionDispatch::Request because + # Rack::Test::Methods enables testing of this module. expect_next_instance_of(Rack::Request) do |rack_request| expect(rack_request).to receive(:params).and_return('jwt' => payload) end diff --git a/spec/lib/sentry/client_spec.rb b/spec/lib/sentry/client_spec.rb new file mode 100644 index 00000000000..b36be0fd9c1 --- /dev/null +++ b/spec/lib/sentry/client_spec.rb @@ -0,0 +1,119 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Sentry::Client do + let(:issue_status) { 'unresolved' } + let(:limit) { 20 } + let(:sentry_url) { 'https://sentrytest.gitlab.com/api/0/projects/sentry-org/sentry-project' } + let(:token) { 'test-token' } + + let(:sample_response) do + Gitlab::Utils.deep_indifferent_access( + JSON.parse(File.read(Rails.root.join('spec/fixtures/sentry/issues_sample_response.json'))) + ) + end + + subject(:client) { described_class.new(sentry_url, token) } + + describe '#list_issues' do + subject { client.list_issues(issue_status: issue_status, limit: limit) } + + before do + stub_sentry_request(sentry_url + '/issues/?limit=20&query=is:unresolved', body: sample_response) + end + + it 'returns objects of type ErrorTracking::Error' do + expect(subject.length).to eq(1) + expect(subject[0]).to be_a(Gitlab::ErrorTracking::Error) + end + + context 'error object created from sentry response' do + using RSpec::Parameterized::TableSyntax + + where(:error_object, :sentry_response) do + :id | :id + :first_seen | :firstSeen + :last_seen | :lastSeen + :title | :title + :type | :type + :user_count | :userCount + :count | :count + :message | [:metadata, :value] + :culprit | :culprit + :short_id | :shortId + :status | :status + :frequency | [:stats, '24h'] + :project_id | [:project, :id] + :project_name | [:project, :name] + :project_slug | [:project, :slug] + end + + with_them do + it { expect(subject[0].public_send(error_object)).to eq(sample_response[0].dig(*sentry_response)) } + end + + context 'external_url' do + it 'is constructed correctly' do + expect(subject[0].external_url).to eq('https://sentrytest.gitlab.com/sentry-org/sentry-project/issues/11') + end + end + end + + context 'redirects' do + let(:redirect_to) { 'https://redirected.example.com' } + let(:other_url) { 'https://other.example.org' } + + let!(:redirected_req_stub) { stub_sentry_request(other_url) } + + let!(:redirect_req_stub) do + stub_sentry_request( + sentry_url + '/issues/?limit=20&query=is:unresolved', + status: 302, + headers: { location: redirect_to } + ) + end + + it 'does not follow redirects' do + expect { subject }.to raise_exception(Sentry::Client::Error, 'Sentry response error: 302') + expect(redirect_req_stub).to have_been_requested + expect(redirected_req_stub).not_to have_been_requested + end + end + + # Sentry API returns 404 if there are extra slashes in the URL! + context 'extra slashes in URL' do + let(:sentry_url) { 'https://sentrytest.gitlab.com/api/0/projects//sentry-org/sentry-project/' } + let(:client) { described_class.new(sentry_url, token) } + + let!(:valid_req_stub) do + stub_sentry_request( + 'https://sentrytest.gitlab.com/api/0/projects/sentry-org/sentry-project/' \ + 'issues/?limit=20&query=is:unresolved' + ) + end + + it 'removes extra slashes in api url' do + expect(Gitlab::HTTP).to receive(:get).with( + URI('https://sentrytest.gitlab.com/api/0/projects/sentry-org/sentry-project/issues/'), + anything + ).and_call_original + + client.list_issues(issue_status: issue_status, limit: limit) + + expect(valid_req_stub).to have_been_requested + end + end + end + + private + + def stub_sentry_request(url, body: {}, status: 200, headers: {}) + WebMock.stub_request(:get, url) + .to_return( + status: status, + headers: { 'Content-Type' => 'application/json' }.merge(headers), + body: body.to_json + ) + end +end diff --git a/spec/migrations/add_foreign_keys_to_todos_spec.rb b/spec/migrations/add_foreign_keys_to_todos_spec.rb index bf2fa5c0f56..efd87173b9c 100644 --- a/spec/migrations/add_foreign_keys_to_todos_spec.rb +++ b/spec/migrations/add_foreign_keys_to_todos_spec.rb @@ -3,9 +3,11 @@ require Rails.root.join('db', 'migrate', '20180201110056_add_foreign_keys_to_tod describe AddForeignKeysToTodos, :migration do let(:todos) { table(:todos) } + let(:users) { table(:users) } + let(:projects) { table(:projects) } - let(:project) { create(:project) } # rubocop:disable RSpec/FactoriesInMigrationSpecs - let(:user) { create(:user) } # rubocop:disable RSpec/FactoriesInMigrationSpecs + let(:project) { projects.create!(name: 'gitlab', path: 'gitlab-org/gitlab-ce', namespace_id: 1) } + let(:user) { users.create!(email: 'email@email.com', name: 'foo', username: 'foo', projects_limit: 0) } context 'add foreign key on user_id' do let!(:todo_with_user) { create_todo(user_id: user.id) } diff --git a/spec/migrations/cleanup_legacy_artifact_migration_spec.rb b/spec/migrations/cleanup_legacy_artifact_migration_spec.rb new file mode 100644 index 00000000000..dc269d32e5a --- /dev/null +++ b/spec/migrations/cleanup_legacy_artifact_migration_spec.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +require 'spec_helper' +require Rails.root.join('db', 'migrate', '20190104182041_cleanup_legacy_artifact_migration.rb') + +describe CleanupLegacyArtifactMigration, :migration, :sidekiq, :redis do + let(:migration) { spy('migration') } + + context 'when still legacy artifacts exist' do + let(:namespaces) { table(:namespaces) } + let(:projects) { table(:projects) } + let(:pipelines) { table(:ci_pipelines) } + let(:jobs) { table(:ci_builds) } + let(:job_artifacts) { table(:ci_job_artifacts) } + let(:namespace) { namespaces.create!(name: 'gitlab', path: 'gitlab-org') } + let(:project) { projects.create!(name: 'gitlab', path: 'gitlab-ce', namespace_id: namespace.id) } + let(:pipeline) { pipelines.create!(project_id: project.id, ref: 'master', sha: 'adf43c3a') } + let(:archive_file_type) { Gitlab::BackgroundMigration::MigrateLegacyArtifacts::ARCHIVE_FILE_TYPE } + let(:metadata_file_type) { Gitlab::BackgroundMigration::MigrateLegacyArtifacts::METADATA_FILE_TYPE } + let(:local_store) { ::ObjectStorage::Store::LOCAL } + let(:remote_store) { ::ObjectStorage::Store::REMOTE } + let(:legacy_location) { Gitlab::BackgroundMigration::MigrateLegacyArtifacts::LEGACY_PATH_FILE_LOCATION } + + before do + jobs.create!(id: 1, commit_id: pipeline.id, project_id: project.id, status: :success, artifacts_file: 'archive.zip') + jobs.create!(id: 2, commit_id: pipeline.id, project_id: project.id, status: :failed, artifacts_metadata: 'metadata.gz') + jobs.create!(id: 3, commit_id: pipeline.id, project_id: project.id, status: :failed, artifacts_file: 'archive.zip', artifacts_metadata: 'metadata.gz') + jobs.create!(id: 4, commit_id: pipeline.id, project_id: project.id, status: :running) + jobs.create!(id: 5, commit_id: pipeline.id, project_id: project.id, status: :success, artifacts_file: 'archive.zip', artifacts_file_store: remote_store, artifacts_metadata: 'metadata.gz') + jobs.create!(id: 6, commit_id: pipeline.id, project_id: project.id, status: :failed, artifacts_file: 'archive.zip', artifacts_metadata: 'metadata.gz') + end + + it 'steals sidekiq jobs from MigrateLegacyArtifacts background migration' do + expect(Gitlab::BackgroundMigration).to receive(:steal).with('MigrateLegacyArtifacts') + + migrate! + end + + it 'migrates legacy artifacts to ci_job_artifacts table' do + migrate! + + expect(job_artifacts.order(:job_id, :file_type).pluck('project_id, job_id, file_type, file_store, size, expire_at, file, file_sha256, file_location')) + .to eq([[project.id, 1, archive_file_type, local_store, nil, nil, 'archive.zip', nil, legacy_location], + [project.id, 3, archive_file_type, local_store, nil, nil, 'archive.zip', nil, legacy_location], + [project.id, 3, metadata_file_type, local_store, nil, nil, 'metadata.gz', nil, legacy_location], + [project.id, 5, archive_file_type, remote_store, nil, nil, 'archive.zip', nil, legacy_location], + [project.id, 5, metadata_file_type, local_store, nil, nil, 'metadata.gz', nil, legacy_location], + [project.id, 6, archive_file_type, local_store, nil, nil, 'archive.zip', nil, legacy_location], + [project.id, 6, metadata_file_type, local_store, nil, nil, 'metadata.gz', nil, legacy_location]]) + end + end +end diff --git a/spec/migrations/cleanup_namespaceless_pending_delete_projects_spec.rb b/spec/migrations/cleanup_namespaceless_pending_delete_projects_spec.rb index b5980cb9ddb..651341906c2 100644 --- a/spec/migrations/cleanup_namespaceless_pending_delete_projects_spec.rb +++ b/spec/migrations/cleanup_namespaceless_pending_delete_projects_spec.rb @@ -2,6 +2,8 @@ require 'spec_helper' require Rails.root.join('db', 'post_migrate', '20170502101023_cleanup_namespaceless_pending_delete_projects.rb') describe CleanupNamespacelessPendingDeleteProjects, :migration, schema: 20180222043024 do + let(:projects) { table(:projects) } + before do # Stub after_save callbacks that will fail when Project has no namespace allow_any_instance_of(Project).to receive(:ensure_storage_path_exists).and_return(nil) @@ -10,9 +12,9 @@ describe CleanupNamespacelessPendingDeleteProjects, :migration, schema: 20180222 describe '#up' do it 'only cleans up pending delete projects' do - create(:project) # rubocop:disable RSpec/FactoriesInMigrationSpecs - create(:project, pending_delete: true) # rubocop:disable RSpec/FactoriesInMigrationSpecs - project = build(:project, pending_delete: true, namespace_id: nil) # rubocop:disable RSpec/FactoriesInMigrationSpecs + projects.create!(name: 'gitlab', path: 'gitlab-org/gitlab-ce', namespace_id: 1) + projects.create!(name: 'gitlab', path: 'gitlab-org/gitlab-ee', namespace_id: 2, pending_delete: true) + project = Project.new(pending_delete: true, namespace_id: nil) project.save(validate: false) expect(NamespacelessProjectDestroyWorker).to receive(:bulk_perform_async).with([[project.id]]) @@ -21,8 +23,8 @@ describe CleanupNamespacelessPendingDeleteProjects, :migration, schema: 20180222 end it 'does nothing when no pending delete projects without namespace found' do - create(:project) # rubocop:disable RSpec/FactoriesInMigrationSpecs - create(:project, pending_delete: true) # rubocop:disable RSpec/FactoriesInMigrationSpecs + projects.create!(name: 'gitlab', path: 'gitlab-org/gitlab-ce', namespace_id: 1) + projects.create!(name: 'gitlab', path: 'gitlab-org/gitlab-ee', namespace_id: 2, pending_delete: true) expect(NamespacelessProjectDestroyWorker).not_to receive(:bulk_perform_async) diff --git a/spec/models/clusters/applications/ingress_spec.rb b/spec/models/clusters/applications/ingress_spec.rb index de313a8ca36..52c347229c6 100644 --- a/spec/models/clusters/applications/ingress_spec.rb +++ b/spec/models/clusters/applications/ingress_spec.rb @@ -35,7 +35,7 @@ describe Clusters::Applications::Ingress do let(:application) { create(:clusters_applications_ingress, :scheduled, version: '0.22.0') } it 'updates the application version' do - expect(application.reload.version).to eq('0.23.0') + expect(application.reload.version).to eq('1.1.2') end end end @@ -90,7 +90,7 @@ describe Clusters::Applications::Ingress do it 'should be initialized with ingress arguments' do expect(subject.name).to eq('ingress') expect(subject.chart).to eq('stable/nginx-ingress') - expect(subject.version).to eq('0.23.0') + expect(subject.version).to eq('1.1.2') expect(subject).to be_rbac expect(subject.files).to eq(ingress.files) end @@ -107,7 +107,7 @@ describe Clusters::Applications::Ingress do let(:ingress) { create(:clusters_applications_ingress, :errored, version: 'nginx') } it 'should be initialized with the locked version' do - expect(subject.version).to eq('0.23.0') + expect(subject.version).to eq('1.1.2') end end end diff --git a/spec/models/clusters/applications/knative_spec.rb b/spec/models/clusters/applications/knative_spec.rb index 0cf9e10ce04..35818be8deb 100644 --- a/spec/models/clusters/applications/knative_spec.rb +++ b/spec/models/clusters/applications/knative_spec.rb @@ -149,6 +149,35 @@ describe Clusters::Applications::Knative do it { is_expected.to validate_presence_of(:hostname) } end + describe '#service_pod_details' do + let(:cluster) { create(:cluster, :project, :provided_by_gcp) } + let(:service) { cluster.platform_kubernetes } + let(:knative) { create(:clusters_applications_knative, cluster: cluster) } + + let(:namespace) do + create(:cluster_kubernetes_namespace, + cluster: cluster, + cluster_project: cluster.cluster_project, + project: cluster.cluster_project.project) + end + + before do + stub_kubeclient_discover(service.api_url) + stub_kubeclient_knative_services + stub_kubeclient_service_pods + stub_reactive_cache(knative, + { + services: kube_response(kube_knative_services_body), + pods: kube_response(kube_knative_pods_body(cluster.cluster_project.project.name, namespace.namespace)) + }) + synchronous_reactive_cache(knative) + end + + it 'should be able k8s core for pod details' do + expect(knative.service_pod_details(namespace.namespace, cluster.cluster_project.project.name)).not_to be_nil + end + end + describe '#services' do let(:cluster) { create(:cluster, :project, :provided_by_gcp) } let(:service) { cluster.platform_kubernetes } @@ -166,6 +195,7 @@ describe Clusters::Applications::Knative do before do stub_kubeclient_discover(service.api_url) stub_kubeclient_knative_services + stub_kubeclient_service_pods end it 'should have an unintialized cache' do @@ -174,7 +204,11 @@ describe Clusters::Applications::Knative do context 'when using synchronous reactive cache' do before do - stub_reactive_cache(knative, services: kube_response(kube_knative_services_body)) + stub_reactive_cache(knative, + { + services: kube_response(kube_knative_services_body), + pods: kube_response(kube_knative_pods_body(cluster.cluster_project.project.name, namespace.namespace)) + }) synchronous_reactive_cache(knative) end diff --git a/spec/models/concerns/manual_inverse_association_spec.rb b/spec/models/concerns/manual_inverse_association_spec.rb index aad40883854..ff4a04ea573 100644 --- a/spec/models/concerns/manual_inverse_association_spec.rb +++ b/spec/models/concerns/manual_inverse_association_spec.rb @@ -32,10 +32,10 @@ describe ManualInverseAssociation do .not_to exceed_query_limit(0) end - it 'passes arguments to the default association method, to allow reloading' do + it 'allows reloading the relation' do query_count = ActiveRecord::QueryRecorder.new do instance.manual_association - instance.manual_association(true) + instance.reload_manual_association end.count expect(query_count).to eq(2) diff --git a/spec/models/error_tracking/project_error_tracking_setting_spec.rb b/spec/models/error_tracking/project_error_tracking_setting_spec.rb index 83f29718eda..2f8ab21d4b2 100644 --- a/spec/models/error_tracking/project_error_tracking_setting_spec.rb +++ b/spec/models/error_tracking/project_error_tracking_setting_spec.rb @@ -3,33 +3,106 @@ require 'spec_helper' describe ErrorTracking::ProjectErrorTrackingSetting do + include ReactiveCachingHelpers + set(:project) { create(:project) } + subject { create(:project_error_tracking_setting, project: project) } + describe 'Associations' do it { is_expected.to belong_to(:project) } end describe 'Validations' do - subject { create(:project_error_tracking_setting, project: project) } - context 'when api_url is over 255 chars' do - before do + it 'fails validation' do subject.api_url = 'https://' + 'a' * 250 - end - it 'fails validation' do expect(subject).not_to be_valid expect(subject.errors.messages[:api_url]).to include('is too long (maximum is 255 characters)') end end context 'With unsafe url' do - let(:project_error_tracking_setting) { create(:project_error_tracking_setting, project: project) } - it 'fails validation' do - project_error_tracking_setting.api_url = "https://replaceme.com/'><script>alert(document.cookie)</script>" + subject.api_url = "https://replaceme.com/'><script>alert(document.cookie)</script>" + + expect(subject).not_to be_valid + end + end + + context 'URL path' do + it 'fails validation with wrong path' do + subject.api_url = 'http://gitlab.com/project1/something' + + expect(subject).not_to be_valid + expect(subject.errors.messages[:api_url]).to include('path needs to start with /api/0/projects') + end + + it 'passes validation with correct path' do + subject.api_url = 'http://gitlab.com/api/0/projects/project1/something' + + expect(subject).to be_valid + end + end + end + + describe '#sentry_external_url' do + let(:sentry_url) { 'https://sentrytest.gitlab.com/api/0/projects/sentry-org/sentry-project' } + + before do + subject.api_url = sentry_url + end + + it 'returns the correct url' do + expect(subject.class).to receive(:extract_sentry_external_url).with(sentry_url).and_call_original + + result = subject.sentry_external_url + + expect(result).to eq('https://sentrytest.gitlab.com/sentry-org/sentry-project') + end + end + + describe '#sentry_client' do + it 'returns sentry client' do + expect(subject.sentry_client).to be_a(Sentry::Client) + end + end + + describe '#list_sentry_issues' do + let(:issues) { [:list, :of, :issues] } + + let(:opts) do + { issue_status: 'unresolved', limit: 10 } + end + + let(:result) do + subject.list_sentry_issues(**opts) + end + + context 'when cached' do + let(:sentry_client) { spy(:sentry_client) } + + before do + stub_reactive_cache(subject, issues, opts) + synchronous_reactive_cache(subject) + + expect(subject).to receive(:sentry_client).and_return(sentry_client) + end + + it 'returns cached issues' do + expect(sentry_client).to receive(:list_issues).with(opts) + .and_return(issues) + + expect(result).to eq(issues: issues) + end + end + + context 'when not cached' do + it 'returns nil' do + expect(subject).not_to receive(:sentry_client) - expect(project_error_tracking_setting).not_to be_valid + expect(result).to be_nil end end end diff --git a/spec/models/gpg_key_spec.rb b/spec/models/gpg_key_spec.rb index 33e6f1de3d1..58a1d2e4ea2 100644 --- a/spec/models/gpg_key_spec.rb +++ b/spec/models/gpg_key_spec.rb @@ -199,7 +199,7 @@ describe GpgKey do gpg_key.revoke - expect(gpg_key.subkeys(true)).to be_blank + expect(gpg_key.subkeys.reload).to be_blank end it 'invalidates all signatures associated to the subkeys' do diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index e18b29df321..bfc9035cb56 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -1418,6 +1418,23 @@ describe MergeRequest do .to change { merge_request.reload.head_pipeline } .from(nil).to(pipeline) end + + context 'when merge request has already had head pipeline' do + before do + merge_request.update!(head_pipeline: pipeline) + end + + context 'when failed to find an actual head pipeline' do + before do + allow(merge_request).to receive(:find_actual_head_pipeline) { } + end + + it 'does not update the current head pipeline' do + expect { subject } + .not_to change { merge_request.reload.head_pipeline } + end + end + end end context 'when there are no pipelines with the diff head sha' do diff --git a/spec/models/milestone_spec.rb b/spec/models/milestone_spec.rb index b3d31e65c85..2e436f2cc8a 100644 --- a/spec/models/milestone_spec.rb +++ b/spec/models/milestone_spec.rb @@ -240,7 +240,88 @@ describe Milestone do end end - describe '.upcoming_ids_by_projects' do + describe '#for_projects_and_groups' do + let(:project) { create(:project) } + let(:project_other) { create(:project) } + let(:group) { create(:group) } + let(:group_other) { create(:group) } + + before do + create(:milestone, project: project) + create(:milestone, project: project_other) + create(:milestone, group: group) + create(:milestone, group: group_other) + end + + subject { described_class.for_projects_and_groups(projects, groups) } + + shared_examples 'filters by projects and groups' do + it 'returns milestones filtered by project' do + milestones = described_class.for_projects_and_groups(projects, []) + + expect(milestones.count).to eq(1) + expect(milestones.first.project_id).to eq(project.id) + end + + it 'returns milestones filtered by group' do + milestones = described_class.for_projects_and_groups([], groups) + + expect(milestones.count).to eq(1) + expect(milestones.first.group_id).to eq(group.id) + end + + it 'returns milestones filtered by both project and group' do + milestones = described_class.for_projects_and_groups(projects, groups) + + expect(milestones.count).to eq(2) + expect(milestones).to contain_exactly(project.milestones.first, group.milestones.first) + end + end + + context 'ids as params' do + let(:projects) { [project.id] } + let(:groups) { [group.id] } + + it_behaves_like 'filters by projects and groups' + end + + context 'relations as params' do + let(:projects) { Project.where(id: project.id).select(:id) } + let(:groups) { Group.where(id: group.id).select(:id) } + + it_behaves_like 'filters by projects and groups' + end + + context 'objects as params' do + let(:projects) { [project] } + let(:groups) { [group] } + + it_behaves_like 'filters by projects and groups' + end + + it 'returns no records if projects and groups are nil' do + milestones = described_class.for_projects_and_groups(nil, nil) + + expect(milestones).to be_empty + end + end + + describe '.upcoming_ids' do + let(:group_1) { create(:group) } + let(:group_2) { create(:group) } + let(:group_3) { create(:group) } + let(:groups) { [group_1, group_2, group_3] } + + let!(:past_milestone_group_1) { create(:milestone, group: group_1, due_date: Time.now - 1.day) } + let!(:current_milestone_group_1) { create(:milestone, group: group_1, due_date: Time.now + 1.day) } + let!(:future_milestone_group_1) { create(:milestone, group: group_1, due_date: Time.now + 2.days) } + + let!(:past_milestone_group_2) { create(:milestone, group: group_2, due_date: Time.now - 1.day) } + let!(:closed_milestone_group_2) { create(:milestone, :closed, group: group_2, due_date: Time.now + 1.day) } + let!(:current_milestone_group_2) { create(:milestone, group: group_2, due_date: Time.now + 2.days) } + + let!(:past_milestone_group_3) { create(:milestone, group: group_3, due_date: Time.now - 1.day) } + let(:project_1) { create(:project) } let(:project_2) { create(:project) } let(:project_3) { create(:project) } @@ -256,16 +337,20 @@ describe Milestone do let!(:past_milestone_project_3) { create(:milestone, project: project_3, due_date: Time.now - 1.day) } - # The call to `#try` is because this returns a relation with a Postgres DB, - # and an array of IDs with a MySQL DB. - let(:milestone_ids) { described_class.upcoming_ids_by_projects(projects).map { |id| id.try(:id) || id } } + let(:milestone_ids) { described_class.upcoming_ids(projects, groups).map(&:id) } - it 'returns the next upcoming open milestone ID for each project' do - expect(milestone_ids).to contain_exactly(current_milestone_project_1.id, current_milestone_project_2.id) + it 'returns the next upcoming open milestone ID for each project and group' do + expect(milestone_ids).to contain_exactly( + current_milestone_project_1.id, + current_milestone_project_2.id, + current_milestone_group_1.id, + current_milestone_group_2.id + ) end - context 'when the projects have no open upcoming milestones' do + context 'when the projects and groups have no open upcoming milestones' do let(:projects) { [project_3] } + let(:groups) { [group_3] } it 'returns no results' do expect(milestone_ids).to be_empty diff --git a/spec/models/project_import_data_spec.rb b/spec/models/project_import_data_spec.rb index e9910c0a5d1..fe47811f074 100644 --- a/spec/models/project_import_data_spec.rb +++ b/spec/models/project_import_data_spec.rb @@ -39,4 +39,15 @@ describe ProjectImportData do expect(row.credentials).to eq({ 'number' => 10, 'foo' => 'bar' }) end end + + describe '#clear_credentials' do + it 'clears out the Hash' do + row = described_class.new + + row.merge_credentials('number' => 10) + row.clear_credentials + + expect(row.credentials).to eq({}) + end + end end diff --git a/spec/models/project_services/teamcity_service_spec.rb b/spec/models/project_services/teamcity_service_spec.rb index 43a0ed99296..64b4efca43a 100644 --- a/spec/models/project_services/teamcity_service_spec.rb +++ b/spec/models/project_services/teamcity_service_spec.rb @@ -205,7 +205,7 @@ describe TeamcityService, :use_clean_rails_memory_store_caching do end def stub_request(status: 200, body: nil, build_status: 'success') - teamcity_full_url = 'http://gitlab.com/teamcity/httpAuth/app/rest/builds/branch:unspecified:any,number:123' + teamcity_full_url = 'http://gitlab.com/teamcity/httpAuth/app/rest/builds/branch:unspecified:any,revision:123' auth = %w(mic password) body ||= %Q({"build":{"status":"#{build_status}","id":"666"}}) diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index d1ab0bdba29..7a8dc59039e 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -209,9 +209,14 @@ describe Project do it 'does not allow new projects beyond user limits' do project2 = build(:project) - allow(project2).to receive(:creator).and_return(double(can_create_project?: false, projects_limit: 0).as_null_object) + + allow(project2) + .to receive(:creator) + .and_return( + double(can_create_project?: false, projects_limit: 0).as_null_object + ) + expect(project2).not_to be_valid - expect(project2.errors[:limit_reached].first).to match(/Personal project creation is not allowed/) end describe 'wiki path conflict' do @@ -1105,13 +1110,13 @@ describe Project do describe '#pipeline_for' do let(:project) { create(:project, :repository) } - let!(:pipeline) { create_pipeline } + let!(:pipeline) { create_pipeline(project) } shared_examples 'giving the correct pipeline' do it { is_expected.to eq(pipeline) } context 'return latest' do - let!(:pipeline2) { create_pipeline } + let!(:pipeline2) { create_pipeline(project) } it { is_expected.to eq(pipeline2) } end @@ -1128,13 +1133,6 @@ describe Project do it_behaves_like 'giving the correct pipeline' end - - def create_pipeline - create(:ci_pipeline, - project: project, - ref: 'master', - sha: project.commit('master').sha) - end end describe '#builds_enabled' do @@ -1420,6 +1418,24 @@ describe Project do end end + describe '#visibility_level' do + let(:project) { build(:project) } + + subject { project.visibility_level } + + context 'by default' do + it { is_expected.to eq(Gitlab::VisibilityLevel::PRIVATE) } + end + + context 'when set to INTERNAL in application settings' do + before do + stub_application_setting(default_project_visibility: Gitlab::VisibilityLevel::INTERNAL) + end + + it { is_expected.to eq(Gitlab::VisibilityLevel::INTERNAL) } + end + end + describe '#visibility_level_allowed?' do let(:project) { create(:project, :internal) } @@ -1922,38 +1938,21 @@ describe Project do end end - describe '#latest_successful_builds_for and #latest_successful_build_for' do - def create_pipeline(status = 'success') - create(:ci_pipeline, project: project, - sha: project.commit.sha, - ref: project.default_branch, - status: status) - end - - def create_build(new_pipeline = pipeline, name = 'test') - create(:ci_build, :success, :artifacts, - pipeline: new_pipeline, - status: new_pipeline.status, - name: name) - end - + describe '#latest_successful_build_for' do let(:project) { create(:project, :repository) } - let(:pipeline) { create_pipeline } + let(:pipeline) { create_pipeline(project) } context 'with many builds' do it 'gives the latest builds from latest pipeline' do - pipeline1 = create_pipeline - pipeline2 = create_pipeline + pipeline1 = create_pipeline(project) + pipeline2 = create_pipeline(project) create_build(pipeline1, 'test') create_build(pipeline1, 'test2') build1_p2 = create_build(pipeline2, 'test') - build2_p2 = create_build(pipeline2, 'test2') - - latest_builds = project.latest_successful_builds_for - single_build = project.latest_successful_build_for(build1_p2.name) + create_build(pipeline2, 'test2') - expect(latest_builds).to contain_exactly(build2_p2, build1_p2) - expect(single_build).to eq(build1_p2) + expect(project.latest_successful_build_for(build1_p2.name)) + .to eq(build1_p2) end end @@ -1962,51 +1961,90 @@ describe Project do context 'standalone pipeline' do it 'returns builds for ref for default_branch' do - builds = project.latest_successful_builds_for - single_build = project.latest_successful_build_for(build.name) + expect(project.latest_successful_build_for(build.name)) + .to eq(build) + end - expect(builds).to contain_exactly(build) - expect(single_build).to eq(build) + it 'returns empty relation if the build cannot be found' do + expect(project.latest_successful_build_for('TAIL')) + .to be_nil end + end - it 'returns empty relation if the build cannot be found for #latest_successful_builds_for' do - builds = project.latest_successful_builds_for('TAIL') + context 'with some pending pipeline' do + before do + create_build(create_pipeline(project, 'pending')) + end - expect(builds).to be_kind_of(ActiveRecord::Relation) - expect(builds).to be_empty + it 'gives the latest build from latest pipeline' do + expect(project.latest_successful_build_for(build.name)) + .to eq(build) end + end + end + + context 'with pending pipeline' do + it 'returns empty relation' do + pipeline.update(status: 'pending') + pending_build = create_build(pipeline) + + expect(project.latest_successful_build_for(pending_build.name)).to be_nil + end + end + end + + describe '#latest_successful_build_for!' do + let(:project) { create(:project, :repository) } + let(:pipeline) { create_pipeline(project) } + + context 'with many builds' do + it 'gives the latest builds from latest pipeline' do + pipeline1 = create_pipeline(project) + pipeline2 = create_pipeline(project) + create_build(pipeline1, 'test') + create_build(pipeline1, 'test2') + build1_p2 = create_build(pipeline2, 'test') + create_build(pipeline2, 'test2') - it 'returns exception if the build cannot be found for #latest_successful_build_for' do - expect { project.latest_successful_build_for(build.name, 'TAIL') }.to raise_error(ActiveRecord::RecordNotFound) + expect(project.latest_successful_build_for(build1_p2.name)) + .to eq(build1_p2) + end + end + + context 'with succeeded pipeline' do + let!(:build) { create_build } + + context 'standalone pipeline' do + it 'returns builds for ref for default_branch' do + expect(project.latest_successful_build_for!(build.name)) + .to eq(build) + end + + it 'returns exception if the build cannot be found' do + expect { project.latest_successful_build_for!(build.name, 'TAIL') } + .to raise_error(ActiveRecord::RecordNotFound) end end context 'with some pending pipeline' do before do - create_build(create_pipeline('pending')) + create_build(create_pipeline(project, 'pending')) end it 'gives the latest build from latest pipeline' do - latest_builds = project.latest_successful_builds_for - last_single_build = project.latest_successful_build_for(build.name) - - expect(latest_builds).to contain_exactly(build) - expect(last_single_build).to eq(build) + expect(project.latest_successful_build_for!(build.name)) + .to eq(build) end end end context 'with pending pipeline' do - before do - pipeline.update(status: 'pending') - create_build(pipeline) - end - it 'returns empty relation' do - builds = project.latest_successful_builds_for + pipeline.update(status: 'pending') + pending_build = create_build(pipeline) - expect(builds).to be_kind_of(ActiveRecord::Relation) - expect(builds).to be_empty + expect { project.latest_successful_build_for!(pending_build.name) } + .to raise_error(ActiveRecord::RecordNotFound) end end end @@ -3054,7 +3092,7 @@ describe Project do context 'when the project is in a subgroup' do let(:group) { create(:group, :nested) } - it { is_expected.to be(false) } + it { is_expected.to be(true) } end end @@ -4387,7 +4425,101 @@ describe Project do end end + describe '#leave_pool_repository' do + let(:pool) { create(:pool_repository) } + let(:project) { create(:project, :repository, pool_repository: pool) } + + it 'removes the membership' do + project.leave_pool_repository + + expect(pool.member_projects.reload).not_to include(project) + end + end + + describe '#check_personal_projects_limit' do + context 'when creating a project for a group' do + it 'does nothing' do + creator = build(:user) + project = build(:project, namespace: build(:group), creator: creator) + + allow(creator) + .to receive(:can_create_project?) + .and_return(false) + + project.check_personal_projects_limit + + expect(project.errors).to be_empty + end + end + + context 'when the user is not allowed to create a personal project' do + let(:user) { build(:user) } + let(:project) { build(:project, creator: user) } + + before do + allow(user) + .to receive(:can_create_project?) + .and_return(false) + end + + context 'when the project limit is zero' do + it 'adds a validation error' do + allow(user) + .to receive(:projects_limit) + .and_return(0) + + project.check_personal_projects_limit + + expect(project.errors[:limit_reached].first) + .to match(/Personal project creation is not allowed/) + end + end + + context 'when the project limit is greater than zero' do + it 'adds a validation error' do + allow(user) + .to receive(:projects_limit) + .and_return(5) + + project.check_personal_projects_limit + + expect(project.errors[:limit_reached].first) + .to match(/Your project limit is 5 projects/) + end + end + end + + context 'when the user is allowed to create personal projects' do + it 'does nothing' do + user = build(:user) + project = build(:project, creator: user) + + allow(user) + .to receive(:can_create_project?) + .and_return(true) + + project.check_personal_projects_limit + + expect(project.errors).to be_empty + end + end + end + def rugged_config rugged_repo(project.repository).config end + + def create_pipeline(project, status = 'success') + create(:ci_pipeline, project: project, + sha: project.commit.sha, + ref: project.default_branch, + status: status) + end + + def create_build(new_pipeline = pipeline, name = 'test') + create(:ci_build, :success, :artifacts, + pipeline: new_pipeline, + status: new_pipeline.status, + name: name) + end end diff --git a/spec/models/releases/link_spec.rb b/spec/models/releases/link_spec.rb index e88c186cbb8..06ed1438688 100644 --- a/spec/models/releases/link_spec.rb +++ b/spec/models/releases/link_spec.rb @@ -31,6 +31,16 @@ describe Releases::Link do end.to raise_error(ActiveRecord::RecordInvalid) end end + + context 'when duplicate url is added to a release' do + let!(:link) { create(:release_link, url: 'http://gitlab.com', release: release) } + + it 'raises an error' do + expect do + create(:release_link, url: 'http://gitlab.com', release: release) + end.to raise_error(ActiveRecord::RecordInvalid) + end + end end describe '.sorted' do diff --git a/spec/models/remote_mirror_spec.rb b/spec/models/remote_mirror_spec.rb index 224bc9ed935..c06e9a08ab4 100644 --- a/spec/models/remote_mirror_spec.rb +++ b/spec/models/remote_mirror_spec.rb @@ -303,6 +303,25 @@ describe RemoteMirror, :mailer do end end + context '#url=' do + let(:remote_mirror) { create(:project, :repository, :remote_mirror).remote_mirrors.first } + + it 'resets all the columns when URL changes' do + remote_mirror.update(last_error: Time.now, + last_update_at: Time.now, + last_successful_update_at: Time.now, + update_status: 'started', + error_notification_sent: true) + + expect { remote_mirror.update_attribute(:url, 'http://new.example.com') } + .to change { remote_mirror.last_error }.to(nil) + .and change { remote_mirror.last_update_at }.to(nil) + .and change { remote_mirror.last_successful_update_at }.to(nil) + .and change { remote_mirror.update_status }.to('finished') + .and change { remote_mirror.error_notification_sent }.to(false) + end + end + context '#updated_since?' do let(:remote_mirror) { create(:project, :repository, :remote_mirror).remote_mirrors.first } let(:timestamp) { Time.now - 5.minutes } diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 2063b4bbe75..ac5874fd0f7 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -2400,22 +2400,4 @@ describe Repository do repository.merge_base('master', 'fix') end end - - describe '#cache' do - subject(:cache) { repository.send(:cache) } - - it 'returns a RepositoryCache' do - expect(subject).to be_kind_of Gitlab::RepositoryCache - end - - it 'when is_wiki it includes wiki as part of key' do - allow(repository).to receive(:is_wiki) { true } - - expect(subject.namespace).to include('wiki') - end - - it 'when is_wiki is false extra_namespace is nil' do - expect(subject.namespace).not_to include('wiki') - end - end end diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb index 9cb20854f6e..2a4030de998 100644 --- a/spec/policies/project_policy_spec.rb +++ b/spec/policies/project_policy_spec.rb @@ -24,7 +24,7 @@ describe ProjectPolicy do download_code fork_project create_project_snippet update_issue admin_issue admin_label admin_list read_commit_status read_build read_container_image read_pipeline read_environment read_deployment - read_merge_request download_wiki_code + read_merge_request download_wiki_code read_sentry_issue ] end diff --git a/spec/requests/api/features_spec.rb b/spec/requests/api/features_spec.rb index 7d3eff7d32d..22a9e36ca31 100644 --- a/spec/requests/api/features_spec.rb +++ b/spec/requests/api/features_spec.rb @@ -129,6 +129,40 @@ describe API::Features do end end + context 'when enabling for a project by path' do + context 'when the project exists' do + let!(:project) { create(:project) } + + it 'sets the feature gate' do + post api("/features/#{feature_name}", admin), params: { value: 'true', project: project.full_path } + + expect(response).to have_gitlab_http_status(201) + expect(json_response).to eq( + 'name' => 'my_feature', + 'state' => 'conditional', + 'gates' => [ + { 'key' => 'boolean', 'value' => false }, + { 'key' => 'actors', 'value' => ["Project:#{project.id}"] } + ]) + end + end + + context 'when the project does not exist' do + it 'sets no new values' do + post api("/features/#{feature_name}", admin), params: { value: 'true', project: 'mep/to/the/mep/mep' } + + expect(response).to have_gitlab_http_status(201) + expect(json_response).to eq( + "name" => "my_feature", + "state" => "off", + "gates" => [ + { "key" => "boolean", "value" => false } + ] + ) + end + end + end + it 'creates a feature with the given percentage if passed an integer' do post api("/features/#{feature_name}", admin), params: { value: '50' } diff --git a/spec/requests/api/files_spec.rb b/spec/requests/api/files_spec.rb index a0aee937185..9b32dc78274 100644 --- a/spec/requests/api/files_spec.rb +++ b/spec/requests/api/files_spec.rb @@ -183,14 +183,15 @@ describe API::Files do get api(url, current_user), params: params expect(response).to have_gitlab_http_status(200) + expect(headers[Gitlab::Workhorse::DETECT_HEADER]).to eq "true" end - it 'forces attachment content disposition' do + it 'sets inline content disposition by default' do url = route(file_path) + "/raw" get api(url, current_user), params: params - expect(headers['Content-Disposition']).to eq('attachment; filename="popen.rb"') + expect(headers['Content-Disposition']).to eq('inline; filename="popen.rb"') end context 'when mandatory params are not given' do diff --git a/spec/requests/api/import_github_spec.rb b/spec/requests/api/import_github_spec.rb new file mode 100644 index 00000000000..aceff9b4aa6 --- /dev/null +++ b/spec/requests/api/import_github_spec.rb @@ -0,0 +1,56 @@ +require 'spec_helper' + +describe API::ImportGithub do + include ApiHelpers + + let(:token) { "asdasd12345" } + let(:provider) { :github } + let(:access_params) { { github_access_token: token } } + + describe "POST /import/github" do + let(:user) { create(:user) } + let(:project) { create(:project) } + let(:provider_username) { user.username } + let(:provider_user) { OpenStruct.new(login: provider_username) } + let(:provider_repo) do + OpenStruct.new( + name: 'vim', + full_name: "#{provider_username}/vim", + owner: OpenStruct.new(login: provider_username) + ) + end + + before do + Grape::Endpoint.before_each do |endpoint| + allow(endpoint).to receive(:client).and_return(double('client', user: provider_user, repo: provider_repo).as_null_object) + end + end + + it 'returns 201 response when the project is imported successfully' do + allow(Gitlab::LegacyGithubImport::ProjectCreator) + .to receive(:new).with(provider_repo, provider_repo.name, user.namespace, user, access_params, type: provider) + .and_return(double(execute: project)) + + post api("/import/github", user), params: { + target_namespace: user.namespace_path, + personal_access_token: token, + repo_id: 1234 + } + expect(response).to have_gitlab_http_status(201) + expect(json_response).to be_a Hash + expect(json_response['name']).to eq(project.name) + end + + it 'returns 422 response when user can not create projects in the chosen namespace' do + other_namespace = create(:group, name: 'other_namespace') + + post api("/import/github", user), params: { + target_namespace: other_namespace.name, + personal_access_token: token, + repo_id: 1234 + } + + expect(response).to have_gitlab_http_status(422) + end + end +end diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb index 0fd465da4f8..ba7930f6c9d 100644 --- a/spec/requests/api/issues_spec.rb +++ b/spec/requests/api/issues_spec.rb @@ -92,12 +92,10 @@ describe API::Issues do end context "when authenticated" do - let(:first_issue) { json_response.first } - it "returns an array of issues" do get api("/issues", user) - expect_paginated_array_response(size: 2) + 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 @@ -105,23 +103,19 @@ describe API::Issues do it 'returns an array of closed issues' do get api('/issues', user), params: { state: :closed } - expect_paginated_array_response(size: 1) - expect(first_issue['id']).to eq(closed_issue.id) + 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(size: 1) - expect(first_issue['id']).to eq(issue.id) + 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(size: 2) - expect(first_issue['id']).to eq(issue.id) - expect(json_response.second['id']).to eq(closed_issue.id) + expect_paginated_array_response([issue.id, closed_issue.id]) end it 'returns issues assigned to me' do @@ -129,8 +123,7 @@ describe API::Issues do get api('/issues', user2), params: { scope: 'assigned_to_me' } - expect_paginated_array_response(size: 1) - expect(first_issue['id']).to eq(issue2.id) + expect_paginated_array_response(issue2.id) end it 'returns issues assigned to me (kebab-case)' do @@ -138,8 +131,7 @@ describe API::Issues do get api('/issues', user2), params: { scope: 'assigned-to-me' } - expect_paginated_array_response(size: 1) - expect(first_issue['id']).to eq(issue2.id) + expect_paginated_array_response(issue2.id) end it 'returns issues authored by the given author id' do @@ -147,8 +139,7 @@ describe API::Issues do get api('/issues', user), params: { author_id: user2.id, scope: 'all' } - expect_paginated_array_response(size: 1) - expect(first_issue['id']).to eq(issue2.id) + expect_paginated_array_response(issue2.id) end it 'returns issues assigned to the given assignee id' do @@ -156,8 +147,7 @@ describe API::Issues do get api('/issues', user), params: { assignee_id: user2.id, scope: 'all' } - expect_paginated_array_response(size: 1) - expect(first_issue['id']).to eq(issue2.id) + expect_paginated_array_response(issue2.id) end it 'returns issues authored by the given author id and assigned to the given assignee id' do @@ -165,8 +155,7 @@ describe API::Issues do get api('/issues', user), params: { author_id: user2.id, assignee_id: user2.id, scope: 'all' } - expect_paginated_array_response(size: 1) - expect(first_issue['id']).to eq(issue2.id) + expect_paginated_array_response(issue2.id) end it 'returns issues with no assignee' do @@ -174,8 +163,7 @@ describe API::Issues do get api('/issues', user), params: { assignee_id: 0, scope: 'all' } - expect_paginated_array_response(size: 1) - expect(first_issue['id']).to eq(issue2.id) + expect_paginated_array_response(issue2.id) end it 'returns issues with no assignee' do @@ -183,8 +171,7 @@ describe API::Issues do get api('/issues', user), params: { assignee_id: 'None', scope: 'all' } - expect_paginated_array_response(size: 1) - expect(first_issue['id']).to eq(issue2.id) + expect_paginated_array_response(issue2.id) end it 'returns issues with any assignee' do @@ -193,18 +180,17 @@ describe API::Issues do get api('/issues', user), params: { assignee_id: 'Any', scope: 'all' } - expect_paginated_array_response(size: 3) + expect_paginated_array_response([issue.id, confidential_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(size: 2) + expect_paginated_array_response([issue2.id, issue.id]) end it 'returns issues not reacted by the authenticated user' do @@ -213,21 +199,19 @@ describe API::Issues do get api('/issues', user2), params: { my_reaction_emoji: 'None', scope: 'all' } - expect_paginated_array_response(size: 2) + 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(size: 1) - expect(json_response.first['id']).to eq(issue.id) + expect_paginated_array_response(issue.id) end it 'returns issues matching given search string for description' do get api("/issues", user), params: { search: issue.description } - expect_paginated_array_response(size: 1) - expect(first_issue['id']).to eq(issue.id) + expect_paginated_array_response(issue.id) end context 'filtering before a specific date' do @@ -236,15 +220,13 @@ describe API::Issues do it 'returns issues created before a specific date' do get api('/issues?created_before=2000-01-02T00:00:00.060Z', user) - expect(json_response.size).to eq(1) - expect(first_issue['id']).to eq(issue2.id) + 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(json_response.size).to eq(1) - expect(first_issue['id']).to eq(issue2.id) + expect_paginated_array_response(issue2.id) end end @@ -254,23 +236,21 @@ describe API::Issues do it 'returns issues created after a specific date' do get api("/issues?created_after=#{issue2.created_at}", user) - expect(json_response.size).to eq(1) - expect(first_issue['id']).to eq(issue2.id) + 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(json_response.size).to eq(1) - expect(first_issue['id']).to eq(issue2.id) + 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(size: 1) - expect(first_issue['labels']).to eq([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 @@ -282,20 +262,20 @@ describe API::Issues do get api("/issues", user), params: { labels: "#{label.title},#{label_b.title},#{label_c.title}" } - expect_paginated_array_response(size: 1) + 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(size: 0) + 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(size: 1) + expect_paginated_array_response(issue.id) expect(json_response.first['labels']).to eq([label.title]) expect(json_response.first['state']).to eq('opened') end @@ -303,112 +283,96 @@ describe API::Issues do 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(size: 0) + 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(size: 1) - expect(json_response.first['id']).to eq(issue.id) + 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(size: 1) - expect(json_response.first['id']).to eq(closed_issue.id) + 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(size: 1) - expect(json_response.first['id']).to eq(closed_issue.id) + 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(size: 0) + 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(size: 0) + 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(size: 2) - expect(json_response.first['id']).to eq(issue.id) - expect(json_response.second['id']).to eq(closed_issue.id) + 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(size: 1) - expect(json_response.first['id']).to eq(closed_issue.id) + 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(size: 1) - expect(json_response.first['id']).to eq(confidential_issue.id) + 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(size: 1) - expect(json_response.first['id']).to eq(closed_issue.id) + 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: [99999] } - expect_paginated_array_response(size: 0) + expect_paginated_array_response([]) end it 'sorts by created_at descending by default' do get api('/issues', user) - response_dates = json_response.map { |issue| issue['created_at'] } - - expect_paginated_array_response(size: 2) - expect(response_dates).to eq(response_dates.sort.reverse) + expect_paginated_array_response([issue.id, closed_issue.id]) end it 'sorts ascending when requested' do get api('/issues?sort=asc', user) - response_dates = json_response.map { |issue| issue['created_at'] } - - expect_paginated_array_response(size: 2) - expect(response_dates).to eq(response_dates.sort) + 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) - response_dates = json_response.map { |issue| issue['updated_at'] } + issue.touch(:updated_at) - expect_paginated_array_response(size: 2) - expect(response_dates).to eq(response_dates.sort.reverse) + 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) - response_dates = json_response.map { |issue| issue['updated_at'] } + issue.touch(:updated_at) - expect_paginated_array_response(size: 2) - expect(response_dates).to eq(response_dates.sort) + expect_paginated_array_response([closed_issue.id, issue.id]) end it 'matches V4 response schema' do @@ -430,7 +394,8 @@ describe API::Issues do project: group_project, state: :closed, milestone: group_milestone, - updated_at: 3.hours.ago + updated_at: 3.hours.ago, + created_at: 1.day.ago end let!(:group_confidential_issue) do create :issue, @@ -438,7 +403,8 @@ describe API::Issues do project: group_project, author: author, assignees: [assignee], - updated_at: 2.hours.ago + updated_at: 2.hours.ago, + created_at: 2.days.ago end let!(:group_issue) do create :issue, @@ -448,7 +414,8 @@ describe API::Issues do milestone: group_milestone, updated_at: 1.hour.ago, title: issue_title, - description: issue_description + description: issue_description, + created_at: 5.days.ago end let!(:group_label) do create(:label, title: 'group_lbl', color: '#FFAABB', project: group_project) @@ -479,10 +446,7 @@ describe API::Issues do it 'also returns subgroups projects issues' do get api(base_url, user) - issue_ids = json_response.map { |issue| issue['id'] } - - expect_paginated_array_response(size: 5) - expect(issue_ids).to include(issue_1.id, issue_2.id) + expect_paginated_array_response([issue_2.id, issue_1.id, group_closed_issue.id, group_confidential_issue.id, group_issue.id]) end end @@ -490,7 +454,7 @@ describe API::Issues do it 'lists all issues in public projects' do get api(base_url) - expect_paginated_array_response(size: 2) + expect_paginated_array_response([group_closed_issue.id, group_issue.id]) end end @@ -502,65 +466,62 @@ describe API::Issues do it 'returns all group issues (including opened and closed)' do get api(base_url, admin) - expect_paginated_array_response(size: 3) + 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(size: 1) - expect(json_response.first['title']).to eq(group_issue.title) + 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(size: 2) + 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(size: 2) + 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(size: 2) + 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(size: 2) + expect_paginated_array_response([group_confidential_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(size: 1) + 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(size: 0) + 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(size: 1) - expect(json_response.first['id']).to eq(group_issue.id) + 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(size: 1) - expect(json_response.first['id']).to eq(group_issue.id) + expect_paginated_array_response(group_issue.id) end it 'returns an array of labeled issues when all labels matches' do @@ -572,69 +533,64 @@ describe API::Issues do get api(base_url, user), params: { labels: "#{group_label.title},#{label_b.title},#{label_c.title}" } - expect_paginated_array_response(size: 1) + 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(size: 1) + 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: [99999] } - expect_paginated_array_response(size: 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(size: 0) + 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(size: 1) + 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 } - response_ids = json_response.map { |issue| issue['id'] } - - expect_paginated_array_response(size: 2) - expect(response_ids).to contain_exactly(group_closed_issue.id, group_confidential_issue.id) + 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(size: 0) + 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(size: 0) + 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(size: 1) - expect(json_response.first['id']).to eq(group_issue.id) + 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(size: 1) - expect(json_response.first['id']).to eq(group_closed_issue.id) + expect_paginated_array_response(group_closed_issue.id) end it 'returns an array of issues with no milestone' do @@ -642,44 +598,33 @@ describe API::Issues do expect(response).to have_gitlab_http_status(200) - expect_paginated_array_response(size: 1) - expect(json_response.first['id']).to eq(group_confidential_issue.id) + expect_paginated_array_response(group_confidential_issue.id) end it 'sorts by created_at descending by default' do get api(base_url, user) - response_dates = json_response.map { |issue| issue['created_at'] } - - expect_paginated_array_response(size: 3) - expect(response_dates).to eq(response_dates.sort.reverse) + expect_paginated_array_response([group_closed_issue.id, group_confidential_issue.id, group_issue.id]) end it 'sorts ascending when requested' do get api("#{base_url}?sort=asc", user) - response_dates = json_response.map { |issue| issue['created_at'] } - - expect_paginated_array_response(size: 3) - expect(response_dates).to eq(response_dates.sort) + 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) - response_dates = json_response.map { |issue| issue['updated_at'] } + group_issue.touch(:updated_at) - expect_paginated_array_response(size: 3) - expect(response_dates).to eq(response_dates.sort.reverse) + 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 } - response_dates = json_response.map { |issue| issue['updated_at'] } - - expect_paginated_array_response(size: 3) - expect(response_dates).to eq(response_dates.sort) + expect_paginated_array_response([group_closed_issue.id, group_confidential_issue.id, group_issue.id]) end end end @@ -691,8 +636,7 @@ describe API::Issues do it 'returns public project issues' do get api("/projects/#{project.id}/issues") - expect_paginated_array_response(size: 2) - expect(json_response.first['title']).to eq(issue.title) + expect_paginated_array_response([issue.id, closed_issue.id]) end end @@ -731,56 +675,49 @@ describe API::Issues do get api("/projects/#{restricted_project.id}/issues", non_member) - expect_paginated_array_response(size: 0) + 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(size: 2) - expect(json_response.first['title']).to eq(issue.title) + 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(size: 2) - expect(json_response.first['title']).to eq(issue.title) + 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(size: 3) - expect(json_response.first['title']).to eq(issue.title) + expect_paginated_array_response([issue.id, confidential_issue.id, closed_issue.id]) end it 'returns project confidential issues for assignee' do get api("#{base_url}/issues", assignee) - expect_paginated_array_response(size: 3) - expect(json_response.first['title']).to eq(issue.title) + 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(size: 3) - expect(json_response.first['title']).to eq(issue.title) + 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(size: 3) - expect(json_response.first['title']).to eq(issue.title) + 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(size: 1) - expect(json_response.first['labels']).to eq([label.title]) + expect_paginated_array_response(issue.id) end it 'returns an array of labeled issues when all labels matches' do @@ -792,142 +729,117 @@ describe API::Issues do get api("#{base_url}/issues", user), params: { labels: "#{label.title},#{label_b.title},#{label_c.title}" } - expect_paginated_array_response(size: 1) - expect(json_response.first['labels']).to eq([label_c.title, label_b.title, label.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(size: 1) - expect(json_response.first['id']).to eq(issue.id) + 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(size: 1) - expect(json_response.first['id']).to eq(issue.id) + 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(size: 1) - expect(json_response.first['id']).to eq(issue.id) + 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: [99999] } - expect_paginated_array_response(size: 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(size: 0) + 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(size: 1) - expect(json_response.first['id']).to eq(issue.id) + 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 } - response_ids = json_response.map { |issue| issue['id'] } - - expect_paginated_array_response(size: 2) - expect(response_ids).to contain_exactly(closed_issue.id, confidential_issue.id) + 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(size: 0) + 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(size: 0) + 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(size: 0) + 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(size: 2) - expect(json_response.first['id']).to eq(issue.id) - expect(json_response.second['id']).to eq(closed_issue.id) + 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(size: 1) - expect(json_response.first['id']).to eq(closed_issue.id) + 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(size: 1) - expect(json_response.first['id']).to eq(confidential_issue.id) + 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 } - response_ids = json_response.map { |issue| issue['id'] } - - expect_paginated_array_response(size: 2) - expect(response_ids).to contain_exactly(closed_issue.id, issue.id) + expect_paginated_array_response([issue.id, closed_issue.id]) end it 'sorts by created_at descending by default' do get api("#{base_url}/issues", user) - response_dates = json_response.map { |issue| issue['created_at'] } - - expect_paginated_array_response(size: 3) - expect(response_dates).to eq(response_dates.sort.reverse) + expect_paginated_array_response([issue.id, confidential_issue.id, closed_issue.id]) end it 'sorts ascending when requested' do get api("#{base_url}/issues", user), params: { sort: :asc } - response_dates = json_response.map { |issue| issue['created_at'] } - - expect_paginated_array_response(size: 3) - expect(response_dates).to eq(response_dates.sort) + 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 } - response_dates = json_response.map { |issue| issue['updated_at'] } + issue.touch(:updated_at) - expect_paginated_array_response(size: 3) - expect(response_dates).to eq(response_dates.sort.reverse) + 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 } - response_dates = json_response.map { |issue| issue['updated_at'] } - - expect_paginated_array_response(size: 3) - expect(response_dates).to eq(response_dates.sort) + expect_paginated_array_response([closed_issue.id, confidential_issue.id, issue.id]) end end @@ -1828,21 +1740,21 @@ describe API::Issues do it 'return public project issues' do get api("/projects/#{project.id}/issues/#{issue.iid}/closed_by") - expect_paginated_array_response(size: 1) + 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(size: 1) + 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(size: 0) + expect_paginated_array_response([]) end end @@ -1878,7 +1790,7 @@ describe API::Issues do it 'return list of referenced merge requests from issue' do get_related_merge_requests(project.id, issue.iid) - expect_paginated_array_response(size: 1) + expect_paginated_array_response(related_mr.id) end it 'renders 404 if project is not visible' do @@ -1902,15 +1814,14 @@ describe API::Issues do get_related_merge_requests(project.id, issue.iid, user) - expect_paginated_array_response(size: 1) - expect(json_response.first['id']).to eq(related_mr.id) + 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(size: 0) + expect_paginated_array_response([]) end end @@ -1948,13 +1859,6 @@ describe API::Issues do end end - def expect_paginated_array_response(size: nil) - expect(response).to have_gitlab_http_status(200) - expect(response).to include_pagination_headers - expect(json_response).to be_an Array - expect(json_response.length).to eq(size) if size - end - describe 'GET projects/:id/issues/:issue_iid/participants' do it_behaves_like 'issuable participants endpoint' do let(:entity) { issue } diff --git a/spec/requests/api/pipelines_spec.rb b/spec/requests/api/pipelines_spec.rb index eb002de62a2..52599db9a9e 100644 --- a/spec/requests/api/pipelines_spec.rb +++ b/spec/requests/api/pipelines_spec.rb @@ -456,8 +456,8 @@ describe API::Pipelines do expect(json_response['message']).to eq '404 Not found' end - it 'logs an audit event' do - expect { delete api("/projects/#{project.id}/pipelines/#{pipeline.id}", owner) }.to change { SecurityEvent.count }.by(1) + it 'does not log an audit event' do + expect { delete api("/projects/#{project.id}/pipelines/#{pipeline.id}", owner) }.not_to change { SecurityEvent.count } end context 'when the pipeline has jobs' do diff --git a/spec/requests/api/project_clusters_spec.rb b/spec/requests/api/project_clusters_spec.rb index e34164aa66a..9bab1f95150 100644 --- a/spec/requests/api/project_clusters_spec.rb +++ b/spec/requests/api/project_clusters_spec.rb @@ -266,6 +266,23 @@ describe API::ProjectClusters do end end end + + context 'when user tries to add multiple clusters' do + before do + create(:cluster, :provided_by_gcp, :project, + projects: [project]) + + post api("/projects/#{project.id}/clusters/user", current_user), params: cluster_params + end + + it 'should respond with 403' do + expect(response).to have_gitlab_http_status(403) + end + + it 'should return an appropriate message' do + expect(json_response['message']).to include('Instance does not support multiple Kubernetes clusters') + end + end end describe 'PUT /projects/:id/clusters/:cluster_id' do diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index ffe4512fa6f..a01b494f615 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -948,6 +948,7 @@ describe API::Projects do expect(json_response['shared_with_groups'].length).to eq(1) expect(json_response['shared_with_groups'][0]['group_id']).to eq(group.id) expect(json_response['shared_with_groups'][0]['group_name']).to eq(group.name) + expect(json_response['shared_with_groups'][0]['group_full_path']).to eq(group.full_path) expect(json_response['shared_with_groups'][0]['group_access_level']).to eq(link.group_access) expect(json_response['shared_with_groups'][0]['expires_at']).to be_nil expect(json_response['only_allow_merge_if_pipeline_succeeds']).to eq(project.only_allow_merge_if_pipeline_succeeds) @@ -967,6 +968,7 @@ describe API::Projects do expect(json_response['shared_with_groups'].length).to eq(1) expect(json_response['shared_with_groups'][0]['group_id']).to eq(group.id) expect(json_response['shared_with_groups'][0]['group_name']).to eq(group.name) + expect(json_response['shared_with_groups'][0]['group_full_path']).to eq(group.full_path) expect(json_response['shared_with_groups'][0]['group_access_level']).to eq(link.group_access) expect(json_response['shared_with_groups'][0]['expires_at']).to eq(expires_at.to_s) end diff --git a/spec/requests/api/release/links_spec.rb b/spec/requests/api/release/links_spec.rb index 9d62257d470..ba948e37e2f 100644 --- a/spec/requests/api/release/links_spec.rb +++ b/spec/requests/api/release/links_spec.rb @@ -74,16 +74,6 @@ describe API::Release::Links do end end end - - context 'when feature flag is disabled' do - before do - stub_feature_flags(releases_page: false) - end - - it_behaves_like '404 response' do - let(:request) { get api("/projects/#{project.id}/releases/v0.1/assets/links", maintainer) } - end - end end describe 'GET /projects/:id/releases/:tag_name/assets/links/:link_id' do @@ -129,16 +119,6 @@ describe API::Release::Links do end end end - - context 'when feature flag is disabled' do - before do - stub_feature_flags(releases_page: false) - end - - it_behaves_like '404 response' do - let(:request) { get api("/projects/#{project.id}/releases/non_existing_tag/assets/links/#{release_link.id}", maintainer) } - end - end end describe 'POST /projects/:id/releases/:tag_name/assets/links' do @@ -231,19 +211,6 @@ describe API::Release::Links do end end end - - context 'when feature flag is disabled' do - before do - stub_feature_flags(releases_page: false) - end - - it_behaves_like '404 response' do - let(:request) do - post api("/projects/#{project.id}/releases/v0.1/assets/links", maintainer), - params: params - end - end - end end describe 'PUT /projects/:id/releases/:tag_name/assets/links/:link_id' do @@ -328,19 +295,6 @@ describe API::Release::Links do end end end - - context 'when feature flag is disabled' do - before do - stub_feature_flags(releases_page: false) - end - - it_behaves_like '404 response' do - let(:request) do - put api("/projects/#{project.id}/releases/v0.1/assets/links/#{release_link.id}", maintainer), - params: params - end - end - end end describe 'DELETE /projects/:id/releases/:tag_name/assets/links/:link_id' do @@ -401,17 +355,5 @@ describe API::Release::Links do end end end - - context 'when feature flag is disabled' do - before do - stub_feature_flags(releases_page: false) - end - - it_behaves_like '404 response' do - let(:request) do - delete api("/projects/#{project.id}/releases/v0.1/assets/links/#{release_link.id}", maintainer) - end - end - end end end diff --git a/spec/requests/api/releases_spec.rb b/spec/requests/api/releases_spec.rb index 978fa0142c2..811e23fb854 100644 --- a/spec/requests/api/releases_spec.rb +++ b/spec/requests/api/releases_spec.rb @@ -83,18 +83,6 @@ describe API::Releases do end end end - - context 'when feature flag is disabled' do - before do - stub_feature_flags(releases_page: false) - end - - it 'cannot find the API' do - get api("/projects/#{project.id}/releases", maintainer) - - expect(response).to have_gitlab_http_status(:not_found) - end - end end describe 'GET /projects/:id/releases/:tag_name' do @@ -205,18 +193,6 @@ describe API::Releases do end end end - - context 'when feature flag is disabled' do - before do - stub_feature_flags(releases_page: false) - end - - it 'cannot find the API' do - get api("/projects/#{project.id}/releases/v0.1", maintainer) - - expect(response).to have_gitlab_http_status(:not_found) - end - end end describe 'POST /projects/:id/releases' do @@ -458,18 +434,6 @@ describe API::Releases do expect(response).to have_gitlab_http_status(:conflict) end end - - context 'when feature flag is disabled' do - before do - stub_feature_flags(releases_page: false) - end - - it 'cannot find the API' do - post api("/projects/#{project.id}/releases", maintainer), params: params - - expect(response).to have_gitlab_http_status(:not_found) - end - end end describe 'PUT /projects/:id/releases/:tag_name' do @@ -565,19 +529,6 @@ describe API::Releases do end end end - - context 'when feature flag is disabled' do - before do - stub_feature_flags(releases_page: false) - end - - it 'cannot find the API' do - put api("/projects/#{project.id}/releases/v0.1", non_project_member), - params: params - - expect(response).to have_gitlab_http_status(:not_found) - end - end end describe 'DELETE /projects/:id/releases/:tag_name' do @@ -648,17 +599,5 @@ describe API::Releases do end end end - - context 'when feature flag is disabled' do - before do - stub_feature_flags(releases_page: false) - end - - it 'cannot find the API' do - delete api("/projects/#{project.id}/releases/v0.1", non_project_member) - - expect(response).to have_gitlab_http_status(:not_found) - end - end end end diff --git a/spec/requests/api/repositories_spec.rb b/spec/requests/api/repositories_spec.rb index b6b57803a6a..0adc95cfbeb 100644 --- a/spec/requests/api/repositories_spec.rb +++ b/spec/requests/api/repositories_spec.rb @@ -166,12 +166,13 @@ describe API::Repositories do get api(route, current_user) expect(response).to have_gitlab_http_status(200) + expect(headers[Gitlab::Workhorse::DETECT_HEADER]).to eq "true" end - it 'forces attachment content disposition' do + it 'sets inline content disposition by default' do get api(route, current_user) - expect(headers['Content-Disposition']).to eq 'attachment' + expect(headers['Content-Disposition']).to eq 'inline' end context 'when sha does not exist' do diff --git a/spec/requests/api/settings_spec.rb b/spec/requests/api/settings_spec.rb index cfbda63bb30..45fb1562e84 100644 --- a/spec/requests/api/settings_spec.rb +++ b/spec/requests/api/settings_spec.rb @@ -63,7 +63,8 @@ describe API::Settings, 'Settings' do terms: 'Hello world!', performance_bar_allowed_group_path: group.full_path, instance_statistics_visibility_private: true, - diff_max_patch_bytes: 150_000 + diff_max_patch_bytes: 150_000, + default_branch_protection: Gitlab::Access::PROTECTION_DEV_CAN_MERGE } expect(response).to have_gitlab_http_status(200) @@ -88,6 +89,7 @@ describe API::Settings, 'Settings' do expect(json_response['performance_bar_allowed_group_id']).to eq(group.id) expect(json_response['instance_statistics_visibility_private']).to be(true) expect(json_response['diff_max_patch_bytes']).to eq(150_000) + expect(json_response['default_branch_protection']).to eq(Gitlab::Access::PROTECTION_DEV_CAN_MERGE) end end diff --git a/spec/requests/api/wikis_spec.rb b/spec/requests/api/wikis_spec.rb index f5092e8e2b5..6109829aad1 100644 --- a/spec/requests/api/wikis_spec.rb +++ b/spec/requests/api/wikis_spec.rb @@ -22,7 +22,7 @@ describe API::Wikis do context 'when wiki has pages' do let!(:pages) do [create(:wiki_page, wiki: project_wiki, attrs: { title: 'page1', content: 'content of page1' }), - create(:wiki_page, wiki: project_wiki, attrs: { title: 'page2', content: 'content of page2' })] + create(:wiki_page, wiki: project_wiki, attrs: { title: 'page2.with.dot', content: 'content of page2' })] end it 'returns the list of wiki pages without content' do diff --git a/spec/requests/git_http_spec.rb b/spec/requests/git_http_spec.rb index 939e870ec53..5b625fd47be 100644 --- a/spec/requests/git_http_spec.rb +++ b/spec/requests/git_http_spec.rb @@ -387,7 +387,7 @@ describe 'Git HTTP requests' do it "responds with status 401" do expect(Rack::Attack::Allow2Ban).to receive(:filter).and_return(true) - allow_any_instance_of(Rack::Request).to receive(:ip).and_return('1.2.3.4') + allow_any_instance_of(ActionDispatch::Request).to receive(:ip).and_return('1.2.3.4') clone_get(path, env) @@ -548,7 +548,7 @@ describe 'Git HTTP requests' do maxretry = options[:maxretry] - 1 ip = '1.2.3.4' - allow_any_instance_of(Rack::Request).to receive(:ip).and_return(ip) + allow_any_instance_of(ActionDispatch::Request).to receive(:ip).and_return(ip) Rack::Attack::Allow2Ban.reset(ip, options) maxretry.times.each do diff --git a/spec/rubocop/cop/inject_enterprise_edition_module_spec.rb b/spec/rubocop/cop/inject_enterprise_edition_module_spec.rb index 08ffc3c3a53..0ff777388e5 100644 --- a/spec/rubocop/cop/inject_enterprise_edition_module_spec.rb +++ b/spec/rubocop/cop/inject_enterprise_edition_module_spec.rb @@ -19,6 +19,41 @@ describe RuboCop::Cop::InjectEnterpriseEditionModule do SOURCE end + it 'does not flag the use of `prepend EEFoo` in the middle of a file' do + expect_no_offenses(<<~SOURCE) + class Foo + prepend EEFoo + end + SOURCE + end + + it 'flags the use of `prepend EE::Foo::Bar` in the middle of a file' do + expect_offense(<<~SOURCE) + class Foo + prepend EE::Foo::Bar + ^^^^^^^^^^^^^^^^^^^^ Injecting EE modules must be done on the last line of this file, outside of any class or module definitions + end + SOURCE + end + + it 'flags the use of `prepend(EE::Foo::Bar)` in the middle of a file' do + expect_offense(<<~SOURCE) + class Foo + prepend(EE::Foo::Bar) + ^^^^^^^^^^^^^^^^^^^^^ Injecting EE modules must be done on the last line of this file, outside of any class or module definitions + end + SOURCE + end + + it 'flags the use of `prepend EE::Foo::Bar::Baz` in the middle of a file' do + expect_offense(<<~SOURCE) + class Foo + prepend EE::Foo::Bar::Baz + ^^^^^^^^^^^^^^^^^^^^^^^^^ Injecting EE modules must be done on the last line of this file, outside of any class or module definitions + end + SOURCE + end + it 'flags the use of `prepend ::EE` in the middle of a file' do expect_offense(<<~SOURCE) class Foo diff --git a/spec/services/ci/destroy_pipeline_service_spec.rb b/spec/services/ci/destroy_pipeline_service_spec.rb index 097daf67feb..d896f990470 100644 --- a/spec/services/ci/destroy_pipeline_service_spec.rb +++ b/spec/services/ci/destroy_pipeline_service_spec.rb @@ -17,8 +17,8 @@ describe ::Ci::DestroyPipelineService do expect { pipeline.reload }.to raise_error(ActiveRecord::RecordNotFound) end - it 'logs an audit event' do - expect { subject }.to change { SecurityEvent.count }.by(1) + it 'does not log an audit event' do + expect { subject }.not_to change { SecurityEvent.count } end context 'when the pipeline has jobs' do diff --git a/spec/services/ci/process_pipeline_service_spec.rb b/spec/services/ci/process_pipeline_service_spec.rb index 7ce7d2d882a..6674d89518e 100644 --- a/spec/services/ci/process_pipeline_service_spec.rb +++ b/spec/services/ci/process_pipeline_service_spec.rb @@ -762,7 +762,7 @@ describe Ci::ProcessPipelineService, '#execute' do end def manual_actions - pipeline.manual_actions(true) + pipeline.manual_actions.reload end def create_build(name, **opts) diff --git a/spec/services/error_tracking/list_issues_service_spec.rb b/spec/services/error_tracking/list_issues_service_spec.rb new file mode 100644 index 00000000000..d9dab1d705c --- /dev/null +++ b/spec/services/error_tracking/list_issues_service_spec.rb @@ -0,0 +1,87 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe ErrorTracking::ListIssuesService do + set(:user) { create(:user) } + set(:project) { create(:project) } + + let(:sentry_url) { 'https://sentrytest.gitlab.com/api/0/projects/sentry-org/sentry-project' } + let(:token) { 'test-token' } + let(:result) { subject.execute } + + let(:error_tracking_setting) do + create(:project_error_tracking_setting, api_url: sentry_url, token: token, project: project) + end + + subject { described_class.new(project, user) } + + before do + expect(project).to receive(:error_tracking_setting).at_least(:once).and_return(error_tracking_setting) + + project.add_reporter(user) + end + + describe '#execute' do + context 'with authorized user' do + context 'when list_sentry_issues returns issues' do + let(:issues) { [:list, :of, :issues] } + + before do + expect(error_tracking_setting) + .to receive(:list_sentry_issues).and_return(issues: issues) + end + + it 'returns the issues' do + expect(result).to eq(status: :success, issues: issues) + end + end + + context 'when list_sentry_issues returns nil' do + before do + expect(error_tracking_setting) + .to receive(:list_sentry_issues).and_return(nil) + end + + it 'result is not ready' do + expect(result).to eq( + status: :error, http_status: :no_content, message: 'not ready') + end + end + end + + context 'with unauthorized user' do + let(:unauthorized_user) { create(:user) } + + subject { described_class.new(project, unauthorized_user) } + + it 'returns error' do + result = subject.execute + + expect(result).to include(status: :error, message: 'access denied') + end + end + + context 'with error tracking disabled' do + before do + error_tracking_setting.enabled = false + end + + it 'raises error' do + result = subject.execute + + expect(result).to include(status: :error, message: 'not enabled') + end + end + end + + describe '#sentry_external_url' do + let(:external_url) { 'https://sentrytest.gitlab.com/sentry-org/sentry-project' } + + it 'calls ErrorTracking::ProjectErrorTrackingSetting' do + expect(error_tracking_setting).to receive(:sentry_external_url).and_call_original + + subject.external_url + end + end +end diff --git a/spec/services/projects/operations/update_service_spec.rb b/spec/services/projects/operations/update_service_spec.rb index 731be907453..6afae3da80c 100644 --- a/spec/services/projects/operations/update_service_spec.rb +++ b/spec/services/projects/operations/update_service_spec.rb @@ -17,7 +17,7 @@ describe Projects::Operations::UpdateService do { error_tracking_setting_attributes: { enabled: false, - api_url: 'http://url', + api_url: 'http://gitlab.com/api/0/projects/org/project', token: 'token' } } @@ -32,7 +32,7 @@ describe Projects::Operations::UpdateService do project.reload expect(project.error_tracking_setting).not_to be_enabled - expect(project.error_tracking_setting.api_url).to eq('http://url') + expect(project.error_tracking_setting.api_url).to eq('http://gitlab.com/api/0/projects/org/project') expect(project.error_tracking_setting.token).to eq('token') end end @@ -42,7 +42,7 @@ describe Projects::Operations::UpdateService do { error_tracking_setting_attributes: { enabled: true, - api_url: 'http://url', + api_url: 'http://gitlab.com/api/0/projects/org/project', token: 'token' } } @@ -52,7 +52,7 @@ describe Projects::Operations::UpdateService do expect(result[:status]).to eq(:success) expect(project.error_tracking_setting).to be_enabled - expect(project.error_tracking_setting.api_url).to eq('http://url') + expect(project.error_tracking_setting.api_url).to eq('http://gitlab.com/api/0/projects/org/project') expect(project.error_tracking_setting.token).to eq('token') end end diff --git a/spec/services/projects/protect_default_branch_service_spec.rb b/spec/services/projects/protect_default_branch_service_spec.rb new file mode 100644 index 00000000000..c145b2c06c6 --- /dev/null +++ b/spec/services/projects/protect_default_branch_service_spec.rb @@ -0,0 +1,242 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Projects::ProtectDefaultBranchService do + let(:service) { described_class.new(project) } + let(:project) { instance_spy(Project) } + + describe '#execute' do + before do + allow(service) + .to receive(:protect_default_branch) + end + + context 'without a default branch' do + it 'does nothing' do + allow(service) + .to receive(:default_branch) + .and_return(nil) + + service.execute + + expect(service) + .not_to have_received(:protect_default_branch) + end + end + + context 'with a default branch' do + it 'protects the default branch' do + allow(service) + .to receive(:default_branch) + .and_return('master') + + service.execute + + expect(service) + .to have_received(:protect_default_branch) + end + end + end + + describe '#protect_default_branch' do + before do + allow(service) + .to receive(:default_branch) + .and_return('master') + + allow(project) + .to receive(:change_head) + .with('master') + + allow(service) + .to receive(:create_protected_branch) + end + + context 'when branch protection is needed' do + before do + allow(service) + .to receive(:protect_branch?) + .and_return(true) + + allow(service) + .to receive(:create_protected_branch) + end + + it 'changes the HEAD of the project' do + service.protect_default_branch + + expect(project) + .to have_received(:change_head) + end + + it 'protects the default branch' do + service.protect_default_branch + + expect(service) + .to have_received(:create_protected_branch) + end + end + + context 'when branch protection is not needed' do + before do + allow(service) + .to receive(:protect_branch?) + .and_return(false) + end + + it 'changes the HEAD of the project' do + service.protect_default_branch + + expect(project) + .to have_received(:change_head) + end + + it 'does not protect the default branch' do + service.protect_default_branch + + expect(service) + .not_to have_received(:create_protected_branch) + end + end + end + + describe '#create_protected_branch' do + it 'creates the protected branch' do + creator = instance_spy(User) + create_service = instance_spy(ProtectedBranches::CreateService) + access_level = Gitlab::Access::DEVELOPER + params = { + name: 'master', + push_access_levels_attributes: [{ access_level: access_level }], + merge_access_levels_attributes: [{ access_level: access_level }] + } + + allow(project) + .to receive(:creator) + .and_return(creator) + + allow(ProtectedBranches::CreateService) + .to receive(:new) + .with(project, creator, params) + .and_return(create_service) + + allow(service) + .to receive(:push_access_level) + .and_return(access_level) + + allow(service) + .to receive(:merge_access_level) + .and_return(access_level) + + allow(service) + .to receive(:default_branch) + .and_return('master') + + allow(create_service) + .to receive(:execute) + .with(skip_authorization: true) + + service.create_protected_branch + + expect(create_service) + .to have_received(:execute) + end + end + + describe '#protect_branch?' do + context 'when default branch protection is disabled' do + it 'returns false' do + allow(Gitlab::CurrentSettings) + .to receive(:default_branch_protection) + .and_return(Gitlab::Access::PROTECTION_NONE) + + expect(service.protect_branch?).to eq(false) + end + end + + context 'when default branch protection is enabled' do + before do + allow(Gitlab::CurrentSettings) + .to receive(:default_branch_protection) + .and_return(Gitlab::Access::PROTECTION_DEV_CAN_MERGE) + + allow(service) + .to receive(:default_branch) + .and_return('master') + end + + it 'returns false if the branch is already protected' do + allow(ProtectedBranch) + .to receive(:protected?) + .with(project, 'master') + .and_return(true) + + expect(service.protect_branch?).to eq(false) + end + + it 'returns true if the branch is not yet protected' do + allow(ProtectedBranch) + .to receive(:protected?) + .with(project, 'master') + .and_return(false) + + expect(service.protect_branch?).to eq(true) + end + end + end + + describe '#default_branch' do + it 'returns the default branch of the project' do + allow(project) + .to receive(:default_branch) + .and_return('master') + + expect(service.default_branch).to eq('master') + end + end + + describe '#push_access_level' do + context 'when developers can push' do + it 'returns the DEVELOPER access level' do + allow(Gitlab::CurrentSettings) + .to receive(:default_branch_protection) + .and_return(Gitlab::Access::PROTECTION_DEV_CAN_PUSH) + + expect(service.push_access_level).to eq(Gitlab::Access::DEVELOPER) + end + end + + context 'when developers can not push' do + it 'returns the MAINTAINER access level' do + allow(Gitlab::CurrentSettings) + .to receive(:default_branch_protection) + .and_return(Gitlab::Access::PROTECTION_DEV_CAN_MERGE) + + expect(service.push_access_level).to eq(Gitlab::Access::MAINTAINER) + end + end + end + + describe '#merge_access_level' do + context 'when developers can merge' do + it 'returns the DEVELOPER access level' do + allow(Gitlab::CurrentSettings) + .to receive(:default_branch_protection) + .and_return(Gitlab::Access::PROTECTION_DEV_CAN_MERGE) + + expect(service.merge_access_level).to eq(Gitlab::Access::DEVELOPER) + end + end + + context 'when developers can not merge' do + it 'returns the MAINTAINER access level' do + allow(Gitlab::CurrentSettings) + .to receive(:default_branch_protection) + .and_return(Gitlab::Access::PROTECTION_DEV_CAN_PUSH) + + expect(service.merge_access_level).to eq(Gitlab::Access::MAINTAINER) + end + end + end +end diff --git a/spec/services/suggestions/apply_service_spec.rb b/spec/services/suggestions/apply_service_spec.rb index 3a483717756..e5ca1c155ed 100644 --- a/spec/services/suggestions/apply_service_spec.rb +++ b/spec/services/suggestions/apply_service_spec.rb @@ -117,13 +117,184 @@ describe Suggestions::ApplyService do expect(commit.committer_email).to eq(user.commit_email) expect(commit.author_name).to eq(user.name) end + + context 'when it fails to apply because the file was changed' do + it 'returns error message' do + service = instance_double(Files::UpdateService) + + expect(Files::UpdateService).to receive(:new) + .and_return(service) + + allow(service).to receive(:execute) + .and_raise(Files::UpdateService::FileChangedError) + + result = subject.execute(suggestion) + + expect(result).to eq(message: 'The file has been changed', status: :error) + end + end + + context 'when diff ref from position is different from repo diff refs' do + it 'returns error message' do + outdated_refs = Gitlab::Diff::DiffRefs.new(base_sha: 'foo', start_sha: 'bar', head_sha: 'outdated') + + allow(suggestion).to receive(:appliable?) { true } + allow(suggestion.position).to receive(:diff_refs) { outdated_refs } + + result = subject.execute(suggestion) + + expect(result).to eq(message: 'The file has been changed', status: :error) + end + end + + context 'multiple suggestions applied' do + let(:expected_content) do + <<-CONTENT.strip_heredoc + require 'fileutils' + require 'open3' + + module Popen + extend self + + + def popen(cmd, path=nil) + unless cmd.is_a?(Array) + # v1 change + end + + path ||= Dir.pwd + # v1 change + vars = { + "PWD" => path + } + + options = { + chdir: path + } + # v2 change + unless File.directory?(path) + FileUtils.mkdir_p(path) + end + + @cmd_output = "" + # v2 change + + Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr| + @cmd_output << stdout.read + @cmd_output << stderr.read + @cmd_status = wait_thr.value.exitstatus + end + + return @cmd_output, @cmd_status + end + end + CONTENT + end + + let(:merge_request) do + create(:merge_request, source_project: project, + target_project: project) + end + + def create_suggestion(diff, old_line: nil, new_line: nil, from_content:, to_content:, path:) + position = Gitlab::Diff::Position.new(old_path: path, + new_path: path, + old_line: old_line, + new_line: new_line, + diff_refs: diff.diff_refs) + + suggestion_note = create(:diff_note_on_merge_request, noteable: merge_request, + original_position: position, + position: position, + project: project) + create(:suggestion, note: suggestion_note, + from_content: from_content, + to_content: to_content) + end + + def apply_suggestion(suggestion) + suggestion.note.reload + merge_request.reload + merge_request.clear_memoized_shas + + result = subject.execute(suggestion) + refresh = MergeRequests::RefreshService.new(project, user) + refresh.execute(merge_request.diff_head_sha, + suggestion.commit_id, + merge_request.source_branch_ref) + + result + end + + def fetch_raw_diff(suggestion) + project.reload.commit(suggestion.commit_id).diffs.diff_files.first.diff.diff + end + + it 'applies multiple suggestions in subsequent versions correctly' do + diff = merge_request.merge_request_diff + path = 'files/ruby/popen.rb' + + suggestion_1_changes = { old_line: nil, + new_line: 13, + from_content: "\n", + to_content: "# v1 change\n", + path: path } + + suggestion_2_changes = { old_line: 24, + new_line: 31, + from_content: " @cmd_output << stderr.read\n", + to_content: "# v2 change\n", + path: path } + + suggestion_1 = create_suggestion(diff, suggestion_1_changes) + suggestion_2 = create_suggestion(diff, suggestion_2_changes) + + apply_suggestion(suggestion_1) + + suggestion_1_diff = fetch_raw_diff(suggestion_1) + + # rubocop: disable Layout/TrailingWhitespace + expected_suggestion_1_diff = <<-CONTENT.strip_heredoc + @@ -10,7 +10,7 @@ module Popen + end + + path ||= Dir.pwd + - + +# v1 change + vars = { + "PWD" => path + } + CONTENT + # rubocop: enable Layout/TrailingWhitespace + + apply_suggestion(suggestion_2) + + suggestion_2_diff = fetch_raw_diff(suggestion_2) + + # rubocop: disable Layout/TrailingWhitespace + expected_suggestion_2_diff = <<-CONTENT.strip_heredoc + @@ -28,7 +28,7 @@ module Popen + + Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr| + @cmd_output << stdout.read + - @cmd_output << stderr.read + +# v2 change + @cmd_status = wait_thr.value.exitstatus + end + CONTENT + # rubocop: enable Layout/TrailingWhitespace + + expect(suggestion_1_diff.strip).to eq(expected_suggestion_1_diff.strip) + expect(suggestion_2_diff.strip).to eq(expected_suggestion_2_diff.strip) + end + end end context 'fork-project' do let(:project) { create(:project, :public, :repository) } let(:forked_project) do - fork_project_with_submodules(project, user) + fork_project_with_submodules(project, user, repository: project.repository) end let(:merge_request) do diff --git a/spec/sidekiq/cron/job_gem_dependency_spec.rb b/spec/sidekiq/cron/job_gem_dependency_spec.rb index 2e30cf025b0..2e7de75fd08 100644 --- a/spec/sidekiq/cron/job_gem_dependency_spec.rb +++ b/spec/sidekiq/cron/job_gem_dependency_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' describe Sidekiq::Cron::Job do describe 'cron jobs' do - context 'when rufus-scheduler depends on ZoTime or EoTime' do + context 'when Fugit depends on ZoTime or EoTime' do before do described_class .create(name: 'TestCronWorker', @@ -10,7 +10,7 @@ describe Sidekiq::Cron::Job do class: Settings.cron_jobs[:pipeline_schedule_worker]['job_class']) end - it 'does not get "Rufus::Scheduler::ZoTime/EtOrbi::EoTime into an exact number"' do + it 'does not get any errors' do expect { described_class.all.first.should_enque?(Time.now) }.not_to raise_error end end diff --git a/spec/simplecov_env.rb b/spec/simplecov_env.rb index 25ddf932d42..82236bb4201 100644 --- a/spec/simplecov_env.rb +++ b/spec/simplecov_env.rb @@ -1,5 +1,6 @@ require 'simplecov' require 'active_support/core_ext/numeric/time' +require_relative '../lib/gitlab/utils' module SimpleCovEnv extend self @@ -16,8 +17,9 @@ module SimpleCovEnv def configure_job SimpleCov.configure do if ENV['CI_JOB_NAME'] - coverage_dir "coverage/#{ENV['CI_JOB_NAME']}" - command_name ENV['CI_JOB_NAME'] + job_name = Gitlab::Utils.slugify(ENV['CI_JOB_NAME']) + coverage_dir "coverage/#{job_name}" + command_name job_name end if ENV['CI'] diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 89357056c93..72684caad32 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -22,7 +22,7 @@ if rspec_profiling_is_configured && (!ENV.key?('CI') || branch_can_be_profiled) require 'rspec_profiling/rspec' end -if ENV['CI'] && !ENV['NO_KNAPSACK'] +if ENV['CI'] && ENV['KNAPSACK_GENERATE_REPORT'] && !ENV['NO_KNAPSACK'] require 'knapsack' Knapsack::Adapters::RSpecAdapter.bind end @@ -127,6 +127,16 @@ RSpec.configure do |config| .and_return(false) end + config.before(:suite) do + # Set latest release blog post URL for "What's new?" link + Gitlab::ReleaseBlogPost.instance.instance_variable_set(:@url, 'https://about.gitlab.com') + end + + config.before(:example, :quarantine) do + # Skip tests in quarantine unless we explicitly focus on them. + skip('In quarantine') unless config.inclusion_filter[:quarantine] + end + config.before(:example, :request_store) do RequestStore.begin! end diff --git a/spec/support/helpers/api_helpers.rb b/spec/support/helpers/api_helpers.rb index a57a3b2cf34..4a9ce9beb78 100644 --- a/spec/support/helpers/api_helpers.rb +++ b/spec/support/helpers/api_helpers.rb @@ -36,4 +36,11 @@ module ApiHelpers full_path end + + def expect_paginated_array_response(items) + expect(response).to have_gitlab_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + expect(json_response.map { |item| item['id'] }).to eq(Array(items)) + end end diff --git a/spec/support/helpers/kubernetes_helpers.rb b/spec/support/helpers/kubernetes_helpers.rb index e7d97561bfc..6930b809048 100644 --- a/spec/support/helpers/kubernetes_helpers.rb +++ b/spec/support/helpers/kubernetes_helpers.rb @@ -20,6 +20,13 @@ module KubernetesHelpers WebMock.stub_request(:get, api_url + '/apis/serving.knative.dev/v1alpha1').to_return(kube_response(kube_v1alpha1_serving_knative_discovery_body)) end + def stub_kubeclient_service_pods(response = nil) + stub_kubeclient_discover(service.api_url) + pods_url = service.api_url + "/api/v1/pods" + + WebMock.stub_request(:get, pods_url).to_return(response || kube_pods_response) + end + def stub_kubeclient_pods(response = nil) stub_kubeclient_discover(service.api_url) pods_url = service.api_url + "/api/v1/namespaces/#{service.actual_namespace}/pods" @@ -212,6 +219,13 @@ module KubernetesHelpers } end + def kube_knative_pods_body(name, namespace) + { + "kind" => "PodList", + "items" => [kube_knative_pod(name: name, namespace: namespace)] + } + end + def kube_knative_services_body(**options) { "kind" => "List", @@ -242,6 +256,28 @@ module KubernetesHelpers } end + # Similar to a kube_pod, but should contain a running service + def kube_knative_pod(name: "kube-pod", namespace: "default", status: "Running") + { + "metadata" => { + "name" => name, + "namespace" => namespace, + "generate_name" => "generated-name-with-suffix", + "creationTimestamp" => "2016-11-25T19:55:19Z", + "labels" => { + "serving.knative.dev/service" => name + } + }, + "spec" => { + "containers" => [ + { "name" => "container-0" }, + { "name" => "container-1" } + ] + }, + "status" => { "phase" => status } + } + end + def kube_deployment(name: "kube-deployment", app: "valid-deployment-label", track: nil) { "metadata" => { @@ -265,10 +301,10 @@ module KubernetesHelpers def kube_service(name: "kubetest", namespace: "default", domain: "example.com") { "metadata" => { - "creationTimestamp" => "2018-11-21T06:16:33Z", - "name" => name, - "namespace" => namespace, - "selfLink" => "/apis/serving.knative.dev/v1alpha1/namespaces/#{namespace}/services/#{name}" + "creationTimestamp" => "2018-11-21T06:16:33Z", + "name" => name, + "namespace" => namespace, + "selfLink" => "/apis/serving.knative.dev/v1alpha1/namespaces/#{namespace}/services/#{name}" }, "spec" => { "generation" => 2 diff --git a/spec/support/shared_examples/lib/gitlab/background_migration/backfill_project_repositories_examples.rb b/spec/support/shared_examples/lib/gitlab/background_migration/backfill_project_repositories_examples.rb index 1f688c0f9d3..dcf7c1a90c2 100644 --- a/spec/support/shared_examples/lib/gitlab/background_migration/backfill_project_repositories_examples.rb +++ b/spec/support/shared_examples/lib/gitlab/background_migration/backfill_project_repositories_examples.rb @@ -32,11 +32,13 @@ shared_examples 'backfill migration for project repositories' do |storage| it 'inserts rows in a single query' do projects.create!(name: 'foo', path: 'foo', namespace_id: group.id, storage_version: storage_version, repository_storage: shard.name) + group2 = namespaces.create!(name: 'gro', path: 'gro') control_count = ActiveRecord::QueryRecorder.new { described_class.new.perform(1, projects.last.id) } projects.create!(name: 'bar', path: 'bar', namespace_id: group.id, storage_version: storage_version, repository_storage: shard.name) - projects.create!(name: 'zoo', path: 'zoo', namespace_id: group.id, storage_version: storage_version, repository_storage: shard.name) + projects.create!(name: 'top', path: 'top', namespace_id: group.id, storage_version: storage_version, repository_storage: shard.name) + projects.create!(name: 'zoo', path: 'zoo', namespace_id: group2.id, storage_version: storage_version, repository_storage: shard.name) expect { described_class.new.perform(1, projects.last.id) }.not_to exceed_query_limit(control_count) end diff --git a/spec/views/help/instance_configuration.html.haml_spec.rb b/spec/views/help/instance_configuration.html.haml_spec.rb index f30b5881fde..ceb7e34a540 100644 --- a/spec/views/help/instance_configuration.html.haml_spec.rb +++ b/spec/views/help/instance_configuration.html.haml_spec.rb @@ -13,9 +13,9 @@ describe 'help/instance_configuration' do it 'has links to several sections' do render - expect(rendered).to have_link(nil, '#ssh-host-keys-fingerprints') if ssh_settings.any? - expect(rendered).to have_link(nil, '#gitlab-pages') - expect(rendered).to have_link(nil, '#gitlab-ci') + expect(rendered).to have_link(nil, href: '#ssh-host-keys-fingerprints') if ssh_settings.any? + expect(rendered).to have_link(nil, href: '#gitlab-pages') + expect(rendered).to have_link(nil, href: '#gitlab-ci') end it 'has several sections' do diff --git a/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb b/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb index ec20c346234..2852aa380b2 100644 --- a/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb +++ b/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb @@ -51,28 +51,10 @@ describe 'layouts/nav/sidebar/_project' do end describe 'releases entry' do - describe 'when releases feature flag is disabled' do - before do - stub_feature_flags(releases_page: false) - end - - it 'does not render releases link' do - render - - expect(rendered).not_to have_link('Releases', href: project_releases_path(project)) - end - end - - describe 'when releases feature flags is enabled' do - before do - stub_feature_flags(releases_page: true) - end - - it 'renders releases link' do - render + it 'renders releases link' do + render - expect(rendered).to have_link('Releases', href: project_releases_path(project)) - end + expect(rendered).to have_link('Releases', href: project_releases_path(project)) end end end diff --git a/spec/views/projects/commit/show.html.haml_spec.rb b/spec/views/projects/commit/show.html.haml_spec.rb index a9c32122600..d07099489e5 100644 --- a/spec/views/projects/commit/show.html.haml_spec.rb +++ b/spec/views/projects/commit/show.html.haml_spec.rb @@ -54,9 +54,9 @@ describe 'projects/commit/show.html.haml' do end it 'shows that it is in the context of a merge request' do - merge_request_url = diffs_project_merge_request_url(project, merge_request, commit_id: commit.id) + merge_request_url = diffs_project_merge_request_path(project, merge_request, commit_id: commit.id) expect(rendered).to have_content("This commit is part of merge request") - expect(rendered).to have_link(merge_request.to_reference, merge_request_url) + expect(rendered).to have_link(merge_request.to_reference, href: merge_request_url) end end end diff --git a/spec/views/projects/merge_requests/show.html.haml_spec.rb b/spec/views/projects/merge_requests/show.html.haml_spec.rb index b0042be339c..d9bda1a3414 100644 --- a/spec/views/projects/merge_requests/show.html.haml_spec.rb +++ b/spec/views/projects/merge_requests/show.html.haml_spec.rb @@ -32,11 +32,7 @@ describe 'projects/merge_requests/show.html.haml' do assign(:noteable, closed_merge_request) assign(:notes, []) assign(:pipelines, Ci::Pipeline.none) - assign( - :issuable_sidebar, - MergeRequestSerializer.new(current_user: user, project: project) - .represent(closed_merge_request, serializer: 'sidebar') - ) + assign(:issuable_sidebar, serialize_issuable_sidebar(user, project, closed_merge_request)) preload_view_requirements @@ -45,6 +41,33 @@ describe 'projects/merge_requests/show.html.haml' do current_application_settings: Gitlab::CurrentSettings.current_application_settings) end + describe 'merge request assignee sidebar' do + context 'when assignee is allowed to merge' do + it 'does not show a warning icon' do + closed_merge_request.update(assignee_id: user.id) + project.add_maintainer(user) + assign(:issuable_sidebar, serialize_issuable_sidebar(user, project, closed_merge_request)) + + render + + expect(rendered).not_to have_css('.cannot-be-merged') + end + end + + context 'when assignee is not allowed to merge' do + it 'shows a warning icon' do + reporter = create(:user) + project.add_reporter(reporter) + closed_merge_request.update(assignee_id: reporter.id) + assign(:issuable_sidebar, serialize_issuable_sidebar(user, project, closed_merge_request)) + + render + + expect(rendered).to have_css('.cannot-be-merged') + end + end + end + context 'when the merge request is closed' do it 'shows the "Reopen" button' do render @@ -80,4 +103,10 @@ describe 'projects/merge_requests/show.html.haml' do expect(rendered).to have_css('a', visible: false, text: 'Close') end end + + def serialize_issuable_sidebar(user, project, merge_request) + MergeRequestSerializer + .new(current_user: user, project: project) + .represent(closed_merge_request, serializer: 'sidebar') + end end diff --git a/spec/workers/git_garbage_collect_worker_spec.rb b/spec/workers/git_garbage_collect_worker_spec.rb index a159f24f876..4895a968d6e 100644 --- a/spec/workers/git_garbage_collect_worker_spec.rb +++ b/spec/workers/git_garbage_collect_worker_spec.rb @@ -71,6 +71,17 @@ describe GitGarbageCollectWorker do subject.perform(project.id) end + + context 'when the repository has joined a pool' do + let!(:pool) { create(:pool_repository, :ready) } + let(:project) { pool.source_project } + + it 'ensures the repositories are linked' do + expect_any_instance_of(PoolRepository).to receive(:link_repository).once + + subject.perform(project.id) + end + end end context 'when no lease can be obtained' do diff --git a/spec/workers/remote_mirror_notification_worker_spec.rb b/spec/workers/remote_mirror_notification_worker_spec.rb new file mode 100644 index 00000000000..e3db10ed645 --- /dev/null +++ b/spec/workers/remote_mirror_notification_worker_spec.rb @@ -0,0 +1,39 @@ +require 'spec_helper' + +describe RemoteMirrorNotificationWorker, :mailer do + set(:project) { create(:project, :repository, :remote_mirror) } + set(:mirror) { project.remote_mirrors.first } + + describe '#execute' do + it 'calls NotificationService#remote_mirror_update_failed when the mirror exists' do + mirror.update_column(:last_error, "There was a problem fetching") + + expect(NotificationService).to receive_message_chain(:new, :remote_mirror_update_failed) + + subject.perform(mirror.id) + + expect(mirror.reload.error_notification_sent?).to be_truthy + end + + it 'does nothing when the mirror has no errors' do + expect(NotificationService).not_to receive(:new) + + subject.perform(mirror.id) + end + + it 'does nothing when the mirror does not exist' do + expect(NotificationService).not_to receive(:new) + + subject.perform(RemoteMirror.maximum(:id).to_i.succ) + end + + it 'does nothing when a notification has already been sent' do + mirror.update_columns(last_error: "There was a problem fetching", + error_notification_sent: true) + + expect(NotificationService).not_to receive(:new) + + subject.perform(mirror.id) + end + end +end diff --git a/spec/workers/repository_update_remote_mirror_worker_spec.rb b/spec/workers/repository_update_remote_mirror_worker_spec.rb index d73b0b53713..b582a3650b6 100644 --- a/spec/workers/repository_update_remote_mirror_worker_spec.rb +++ b/spec/workers/repository_update_remote_mirror_worker_spec.rb @@ -22,6 +22,13 @@ describe RepositoryUpdateRemoteMirrorWorker do expect { subject.perform(remote_mirror.id, Time.now) }.to change { remote_mirror.reload.update_status }.to('finished') end + it 'resets the notification flag upon success' do + expect_any_instance_of(Projects::UpdateRemoteMirrorService).to receive(:execute).with(remote_mirror).and_return(status: :success) + remote_mirror.update_column(:error_notification_sent, true) + + expect { subject.perform(remote_mirror.id, Time.now) }.to change { remote_mirror.reload.error_notification_sent }.to(false) + end + it 'sets status as failed when update remote mirror service executes with errors' do error_message = 'fail!' diff --git a/yarn.lock b/yarn.lock index 7bf59f9de94..fadfeb3dc49 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9,32 +9,32 @@ dependencies: "@babel/highlight" "^7.0.0" -"@babel/core@^7.1.2": - version "7.1.2" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.1.2.tgz#f8d2a9ceb6832887329a7b60f9d035791400ba4e" - integrity sha512-IFeSSnjXdhDaoysIlev//UzHZbdEmm7D0EIH2qtse9xK7mXEZQpYjs2P00XlP1qYsYvid79p+Zgg6tz1mp6iVw== +"@babel/core@^7.2.2": + version "7.2.2" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.2.2.tgz#07adba6dde27bb5ad8d8672f15fde3e08184a687" + integrity sha512-59vB0RWt09cAct5EIe58+NzGP4TFSD3Bz//2/ELy3ZeTeKF6VTD1AXlH8BGGbCX0PuobZBsIzO7IAI9PH67eKw== dependencies: "@babel/code-frame" "^7.0.0" - "@babel/generator" "^7.1.2" - "@babel/helpers" "^7.1.2" - "@babel/parser" "^7.1.2" - "@babel/template" "^7.1.2" - "@babel/traverse" "^7.1.0" - "@babel/types" "^7.1.2" + "@babel/generator" "^7.2.2" + "@babel/helpers" "^7.2.0" + "@babel/parser" "^7.2.2" + "@babel/template" "^7.2.2" + "@babel/traverse" "^7.2.2" + "@babel/types" "^7.2.2" convert-source-map "^1.1.0" - debug "^3.1.0" - json5 "^0.5.0" + debug "^4.1.0" + json5 "^2.1.0" lodash "^4.17.10" resolve "^1.3.2" semver "^5.4.1" source-map "^0.5.0" -"@babel/generator@^7.0.0", "@babel/generator@^7.1.2": - version "7.1.2" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.1.2.tgz#fde75c072575ce7abbd97322e8fef5bae67e4630" - integrity sha512-70A9HWLS/1RHk3Ck8tNHKxOoKQuSKocYgwDN85Pyl/RBduss6AKxUR7RIZ/lzduQMSYfWEM4DDBu6A+XGbkFig== +"@babel/generator@^7.0.0", "@babel/generator@^7.2.2": + version "7.2.2" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.2.2.tgz#18c816c70962640eab42fe8cae5f3947a5c65ccc" + integrity sha512-I4o675J/iS8k+P38dvJ3IBGqObLXyQLTxtrR4u9cSUJOURvafeEWb/pFMOTwtNrmq73mJzyF6ueTbO1BtN0Zeg== dependencies: - "@babel/types" "^7.1.2" + "@babel/types" "^7.2.2" jsesc "^2.5.1" lodash "^4.17.10" source-map "^0.5.0" @@ -64,6 +64,17 @@ "@babel/traverse" "^7.1.0" "@babel/types" "^7.0.0" +"@babel/helper-create-class-features-plugin@^7.2.3": + version "7.2.3" + resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.2.3.tgz#f6e719abb90cb7f4a69591e35fd5eb89047c4a7c" + integrity sha512-xO/3Gn+2C7/eOUeb0VRnSP1+yvWHNxlpAot1eMhtoKDCN7POsyQP5excuT5UsV5daHxMWBeIIOeI5cmB8vMRgQ== + 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-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" @@ -160,14 +171,14 @@ "@babel/traverse" "^7.1.0" "@babel/types" "^7.0.0" -"@babel/helper-replace-supers@^7.1.0": - version "7.1.0" - resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.1.0.tgz#5fc31de522ec0ef0899dc9b3e7cf6a5dd655f362" - integrity sha512-BvcDWYZRWVuDeXTYZWxekQNO5D4kO55aArwZOTFXw6rlLQA8ZaDicJR1sO47h+HrnCiDFiww0fSPV0d713KBGQ== +"@babel/helper-replace-supers@^7.1.0", "@babel/helper-replace-supers@^7.2.3": + 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== dependencies: "@babel/helper-member-expression-to-functions" "^7.0.0" "@babel/helper-optimise-call-expression" "^7.0.0" - "@babel/traverse" "^7.1.0" + "@babel/traverse" "^7.2.3" "@babel/types" "^7.0.0" "@babel/helper-simple-access@^7.1.0": @@ -195,14 +206,14 @@ "@babel/traverse" "^7.1.0" "@babel/types" "^7.0.0" -"@babel/helpers@^7.1.2": - version "7.1.2" - resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.1.2.tgz#ab752e8c35ef7d39987df4e8586c63b8846234b5" - integrity sha512-Myc3pUE8eswD73aWcartxB16K6CGmHDv9KxOmD2CeOs/FaEAQodr3VYGmlvOmog60vNQ2w8QbatuahepZwrHiA== +"@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== dependencies: "@babel/template" "^7.1.2" - "@babel/traverse" "^7.1.0" - "@babel/types" "^7.1.2" + "@babel/traverse" "^7.1.5" + "@babel/types" "^7.2.0" "@babel/highlight@^7.0.0": version "7.0.0" @@ -213,149 +224,146 @@ esutils "^2.0.2" js-tokens "^4.0.0" -"@babel/parser@^7.0.0", "@babel/parser@^7.1.0", "@babel/parser@^7.1.2": - version "7.1.2" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.1.2.tgz#85c5c47af6d244fab77bce6b9bd830e38c978409" - integrity sha512-x5HFsW+E/nQalGMw7hu+fvPqnBeBaIr0lWJ2SG0PPL2j+Pm9lYvCrsZJGIgauPIENx0v10INIyFjmSNUD/gSqQ== +"@babel/parser@^7.0.0", "@babel/parser@^7.2.2", "@babel/parser@^7.2.3": + version "7.2.3" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.2.3.tgz#32f5df65744b70888d17872ec106b02434ba1489" + integrity sha512-0LyEcVlfCoFmci8mXx8A5oIkpkOgyo8dRHtxBnK9RRBwxO2+JZPNsqtVEZQ7mJFPxnXF9lfmU24mHOPI0qnlkA== -"@babel/plugin-proposal-async-generator-functions@^7.1.0": - version "7.1.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.1.0.tgz#41c1a702e10081456e23a7b74d891922dd1bb6ce" - integrity sha512-Fq803F3Jcxo20MXUSDdmZZXrPe6BWyGcWBPPNB/M7WaUYESKDeKMOGIxEzQOjGSmW/NWb6UaPZrtTB2ekhB/ew== +"@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" + integrity sha512-+Dfo/SCQqrwx48ptLVGLdE39YtWRuKc/Y9I5Fy0P1DDBB9lsAHpjcEJQt+4IifuSOSTLBKJObJqMvaO1pIE8LQ== dependencies: "@babel/helper-plugin-utils" "^7.0.0" "@babel/helper-remap-async-to-generator" "^7.1.0" - "@babel/plugin-syntax-async-generators" "^7.0.0" + "@babel/plugin-syntax-async-generators" "^7.2.0" -"@babel/plugin-proposal-class-properties@^7.1.0": - version "7.1.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.1.0.tgz#9af01856b1241db60ec8838d84691aa0bd1e8df4" - integrity sha512-/PCJWN+CKt5v1xcGn4vnuu13QDoV+P7NcICP44BoonAJoPSGwVkgrXihFIQGiEjjPlUDBIw1cM7wYFLARS2/hw== +"@babel/plugin-proposal-class-properties@^7.2.3": + version "7.2.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.2.3.tgz#c9e1294363b346cff333007a92080f3203698461" + integrity sha512-FVuQngLoN2iDrpW7LmhPZ2sO4DJxf35FOcwidwB9Ru9tMvI5URthnkVHuG14IStV+TzkMTyLMoOUlSTtrdVwqw== 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-create-class-features-plugin" "^7.2.3" "@babel/helper-plugin-utils" "^7.0.0" - "@babel/helper-replace-supers" "^7.1.0" - "@babel/plugin-syntax-class-properties" "^7.0.0" -"@babel/plugin-proposal-json-strings@^7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.0.0.tgz#3b4d7b5cf51e1f2e70f52351d28d44fc2970d01e" - integrity sha512-kfVdUkIAGJIVmHmtS/40i/fg/AGnw/rsZBCaapY5yjeO5RA9m165Xbw9KMOu2nqXP5dTFjEjHdfNdoVcHv133Q== +"@babel/plugin-proposal-json-strings@^7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.2.0.tgz#568ecc446c6148ae6b267f02551130891e29f317" + integrity sha512-MAFV1CA/YVmYwZG0fBQyXhmj0BHCB5egZHCKWIFVv/XCxAeVGIHfos3SwDck4LvCllENIAg7xMKOG5kH0dzyUg== dependencies: "@babel/helper-plugin-utils" "^7.0.0" - "@babel/plugin-syntax-json-strings" "^7.0.0" + "@babel/plugin-syntax-json-strings" "^7.2.0" -"@babel/plugin-proposal-object-rest-spread@^7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.0.0.tgz#9a17b547f64d0676b6c9cecd4edf74a82ab85e7e" - integrity sha512-14fhfoPcNu7itSen7Py1iGN0gEm87hX/B+8nZPqkdmANyyYWYMY2pjA3r8WXbWVKMzfnSNS0xY8GVS0IjXi/iw== +"@babel/plugin-proposal-object-rest-spread@^7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.2.0.tgz#88f5fec3e7ad019014c97f7ee3c992f0adbf7fb8" + integrity sha512-1L5mWLSvR76XYUQJXkd/EEQgjq8HHRP6lQuZTTg0VA4tTGPpGemmCdAfQIz1rzEuWAm+ecP8PyyEm30jC1eQCg== dependencies: "@babel/helper-plugin-utils" "^7.0.0" - "@babel/plugin-syntax-object-rest-spread" "^7.0.0" + "@babel/plugin-syntax-object-rest-spread" "^7.2.0" -"@babel/plugin-proposal-optional-catch-binding@^7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.0.0.tgz#b610d928fe551ff7117d42c8bb410eec312a6425" - integrity sha512-JPqAvLG1s13B/AuoBjdBYvn38RqW6n1TzrQO839/sIpqLpbnXKacsAgpZHzLD83Sm8SDXMkkrAvEnJ25+0yIpw== +"@babel/plugin-proposal-optional-catch-binding@^7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.2.0.tgz#135d81edb68a081e55e56ec48541ece8065c38f5" + integrity sha512-mgYj3jCcxug6KUcX4OBoOJz3CMrwRfQELPQ5560F70YQUBZB7uac9fqaWamKR1iWUzGiK2t0ygzjTScZnVz75g== dependencies: "@babel/helper-plugin-utils" "^7.0.0" - "@babel/plugin-syntax-optional-catch-binding" "^7.0.0" + "@babel/plugin-syntax-optional-catch-binding" "^7.2.0" -"@babel/plugin-proposal-unicode-property-regex@^7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.0.0.tgz#498b39cd72536cd7c4b26177d030226eba08cd33" - integrity sha512-tM3icA6GhC3ch2SkmSxv7J/hCWKISzwycub6eGsDrFDgukD4dZ/I+x81XgW0YslS6mzNuQ1Cbzh5osjIMgepPQ== +"@babel/plugin-proposal-private-methods@^7.2.3": + version "7.2.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.2.3.tgz#aff0f5436df2c4365938c0309d551984e42c290c" + integrity sha512-jehrt1/TuLdLeBAVEv1VmTCNJcvSj+5Ozp7l21DN19Ylo0ATxpZ5bDk8i4WS9Ngvdgk/YTcqJCTp3uY2lwQoxw== dependencies: + "@babel/helper-create-class-features-plugin" "^7.2.3" "@babel/helper-plugin-utils" "^7.0.0" - "@babel/helper-regex" "^7.0.0" - regexpu-core "^4.2.0" -"@babel/plugin-syntax-async-generators@^7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.0.0.tgz#bf0891dcdbf59558359d0c626fdc9490e20bc13c" - integrity sha512-im7ged00ddGKAjcZgewXmp1vxSZQQywuQXe2B1A7kajjZmDeY/ekMPmWr9zJgveSaQH0k7BcGrojQhcK06l0zA== +"@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== dependencies: "@babel/helper-plugin-utils" "^7.0.0" + "@babel/helper-regex" "^7.0.0" + regexpu-core "^4.2.0" -"@babel/plugin-syntax-class-properties@^7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.0.0.tgz#e051af5d300cbfbcec4a7476e37a803489881634" - integrity sha512-cR12g0Qzn4sgkjrbrzWy2GE7m9vMl/sFkqZ3gIpAQdrvPDnLM8180i+ANDFIXfjHo9aqp0ccJlQ0QNZcFUbf9w== +"@babel/plugin-syntax-async-generators@^7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.2.0.tgz#69e1f0db34c6f5a0cf7e2b3323bf159a76c8cb7f" + integrity sha512-1ZrIRBv2t0GSlcwVoQ6VgSLpLgiN/FVQUzt9znxo7v2Ov4jJrs8RY8tv0wvDmFN3qIdMKWrmMMW6yZ0G19MfGg== dependencies: "@babel/helper-plugin-utils" "^7.0.0" -"@babel/plugin-syntax-dynamic-import@^7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.0.0.tgz#6dfb7d8b6c3be14ce952962f658f3b7eb54c33ee" - integrity sha512-Gt9xNyRrCHCiyX/ZxDGOcBnlJl0I3IWicpZRC4CdC0P5a/I07Ya2OAMEBU+J7GmRFVmIetqEYRko6QYRuKOESw== +"@babel/plugin-syntax-dynamic-import@^7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.2.0.tgz#69c159ffaf4998122161ad8ebc5e6d1f55df8612" + integrity sha512-mVxuJ0YroI/h/tbFTPGZR8cv6ai+STMKNBq0f8hFxsxWjl94qqhsb+wXbpNMDPU3cfR1TIsVFzU3nXyZMqyK4w== dependencies: "@babel/helper-plugin-utils" "^7.0.0" -"@babel/plugin-syntax-import-meta@^7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.0.0.tgz#ca946b73216c29c39a55ef2d739097fee8a85d69" - integrity sha512-FEoGvhXVAiWzpDjyZIlBGzKyNk/lnRPy7aPke3PjVkiAY0QFsvFfkjUg5diRwVfowBA8SJqvFt0ZoXNSjl70hQ== +"@babel/plugin-syntax-import-meta@^7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.2.0.tgz#2333ef4b875553a3bcd1e93f8ebc09f5b9213a40" + integrity sha512-Hq6kFSZD7+PHkmBN8bCpHR6J8QEoCuEV/B38AIQscYjgMZkGlXB7cHNFzP5jR4RCh5545yP1ujHdmO7hAgKtBA== dependencies: "@babel/helper-plugin-utils" "^7.0.0" -"@babel/plugin-syntax-json-strings@^7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.0.0.tgz#0d259a68090e15b383ce3710e01d5b23f3770cbd" - integrity sha512-UlSfNydC+XLj4bw7ijpldc1uZ/HB84vw+U6BTuqMdIEmz/LDe63w/GHtpQMdXWdqQZFeAI9PjnHe/vDhwirhKA== +"@babel/plugin-syntax-json-strings@^7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.2.0.tgz#72bd13f6ffe1d25938129d2a186b11fd62951470" + integrity sha512-5UGYnMSLRE1dqqZwug+1LISpA403HzlSfsg6P9VXU6TBjcSHeNlw4DxDx7LgpF+iKZoOG/+uzqoRHTdcUpiZNg== dependencies: "@babel/helper-plugin-utils" "^7.0.0" -"@babel/plugin-syntax-object-rest-spread@^7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.0.0.tgz#37d8fbcaf216bd658ea1aebbeb8b75e88ebc549b" - integrity sha512-5A0n4p6bIiVe5OvQPxBnesezsgFJdHhSs3uFSvaPdMqtsovajLZ+G2vZyvNe10EzJBWWo3AcHGKhAFUxqwp2dw== +"@babel/plugin-syntax-object-rest-spread@^7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.2.0.tgz#3b7a3e733510c57e820b9142a6579ac8b0dfad2e" + integrity sha512-t0JKGgqk2We+9may3t0xDdmneaXmyxq0xieYcKHxIsrJO64n1OiMWNUtc5gQK1PA0NpdCRrtZp4z+IUaKugrSA== dependencies: "@babel/helper-plugin-utils" "^7.0.0" -"@babel/plugin-syntax-optional-catch-binding@^7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.0.0.tgz#886f72008b3a8b185977f7cb70713b45e51ee475" - integrity sha512-Wc+HVvwjcq5qBg1w5RG9o9RVzmCaAg/Vp0erHCKpAYV8La6I94o4GQAmFYNmkzoMO6gzoOSulpKeSSz6mPEoZw== +"@babel/plugin-syntax-optional-catch-binding@^7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.2.0.tgz#a94013d6eda8908dfe6a477e7f9eda85656ecf5c" + integrity sha512-bDe4xKNhb0LI7IvZHiA13kff0KEfaGX/Hv4lMA9+7TEc63hMNvfKo6ZFpXhKuEp+II/q35Gc4NoMeDZyaUbj9w== dependencies: "@babel/helper-plugin-utils" "^7.0.0" -"@babel/plugin-transform-arrow-functions@^7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.0.0.tgz#a6c14875848c68a3b4b3163a486535ef25c7e749" - integrity sha512-2EZDBl1WIO/q4DIkIp4s86sdp4ZifL51MoIviLY/gG/mLSuOIEg7J8o6mhbxOTvUJkaN50n+8u41FVsr5KLy/w== +"@babel/plugin-transform-arrow-functions@^7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.2.0.tgz#9aeafbe4d6ffc6563bf8f8372091628f00779550" + integrity sha512-ER77Cax1+8/8jCB9fo4Ud161OZzWN5qawi4GusDuRLcDbDG+bIGYY20zb2dfAFdTRGzrfq2xZPvF0R64EHnimg== dependencies: "@babel/helper-plugin-utils" "^7.0.0" -"@babel/plugin-transform-async-to-generator@^7.1.0": - version "7.1.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.1.0.tgz#109e036496c51dd65857e16acab3bafdf3c57811" - integrity sha512-rNmcmoQ78IrvNCIt/R9U+cixUHeYAzgusTFgIAv+wQb9HJU4szhpDD6e5GCACmj/JP5KxuCwM96bX3L9v4ZN/g== +"@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== dependencies: "@babel/helper-module-imports" "^7.0.0" "@babel/helper-plugin-utils" "^7.0.0" "@babel/helper-remap-async-to-generator" "^7.1.0" -"@babel/plugin-transform-block-scoped-functions@^7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.0.0.tgz#482b3f75103927e37288b3b67b65f848e2aa0d07" - integrity sha512-AOBiyUp7vYTqz2Jibe1UaAWL0Hl9JUXEgjFvvvcSc9MVDItv46ViXFw2F7SVt1B5k+KWjl44eeXOAk3UDEaJjQ== +"@babel/plugin-transform-block-scoped-functions@^7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.2.0.tgz#5d3cc11e8d5ddd752aa64c9148d0db6cb79fd190" + integrity sha512-ntQPR6q1/NKuphly49+QiQiTN0O63uOwjdD6dhIjSWBI5xlrbUFh720TIpzBhpnrLfv2tNH/BXvLIab1+BAI0w== dependencies: "@babel/helper-plugin-utils" "^7.0.0" -"@babel/plugin-transform-block-scoping@^7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.0.0.tgz#1745075edffd7cdaf69fab2fb6f9694424b7e9bc" - integrity sha512-GWEMCrmHQcYWISilUrk9GDqH4enf3UmhOEbNbNrlNAX1ssH3MsS1xLOS6rdjRVPgA7XXVPn87tRkdTEoA/dxEg== +"@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== dependencies: "@babel/helper-plugin-utils" "^7.0.0" lodash "^4.17.10" -"@babel/plugin-transform-classes@^7.1.0": - version "7.1.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.1.0.tgz#ab3f8a564361800cbc8ab1ca6f21108038432249" - integrity sha512-rNaqoD+4OCBZjM7VaskladgqnZ1LO6o2UxuWSDzljzW21pN1KXkB7BstAVweZdxQkHAujps5QMNOTWesBciKFg== +"@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== dependencies: "@babel/helper-annotate-as-pure" "^7.0.0" "@babel/helper-define-map" "^7.1.0" @@ -366,95 +374,95 @@ "@babel/helper-split-export-declaration" "^7.0.0" globals "^11.1.0" -"@babel/plugin-transform-computed-properties@^7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.0.0.tgz#2fbb8900cd3e8258f2a2ede909b90e7556185e31" - integrity sha512-ubouZdChNAv4AAWAgU7QKbB93NU5sHwInEWfp+/OzJKA02E6Woh9RVoX4sZrbRwtybky/d7baTUqwFx+HgbvMA== +"@babel/plugin-transform-computed-properties@^7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.2.0.tgz#83a7df6a658865b1c8f641d510c6f3af220216da" + integrity sha512-kP/drqTxY6Xt3NNpKiMomfgkNn4o7+vKxK2DDKcBG9sHj51vHqMBGy8wbDS/J4lMxnqs153/T3+DmCEAkC5cpA== dependencies: "@babel/helper-plugin-utils" "^7.0.0" -"@babel/plugin-transform-destructuring@^7.0.0": - version "7.1.2" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.1.2.tgz#5fa77d473f5a0a3f5266ad7ce2e8c995a164d60a" - integrity sha512-cvToXvp/OsYxtEn57XJu9BvsGSEYjAh9UeUuXpoi7x6QHB7YdWyQ4lRU/q0Fu1IJNT0o0u4FQ1DMQBzJ8/8vZg== +"@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== dependencies: "@babel/helper-plugin-utils" "^7.0.0" -"@babel/plugin-transform-dotall-regex@^7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.0.0.tgz#73a24da69bc3c370251f43a3d048198546115e58" - integrity sha512-00THs8eJxOJUFVx1w8i1MBF4XH4PsAjKjQ1eqN/uCH3YKwP21GCKfrn6YZFZswbOk9+0cw1zGQPHVc1KBlSxig== +"@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== dependencies: "@babel/helper-plugin-utils" "^7.0.0" "@babel/helper-regex" "^7.0.0" regexpu-core "^4.1.3" -"@babel/plugin-transform-duplicate-keys@^7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.0.0.tgz#a0601e580991e7cace080e4cf919cfd58da74e86" - integrity sha512-w2vfPkMqRkdxx+C71ATLJG30PpwtTpW7DDdLqYt2acXU7YjztzeWW2Jk1T6hKqCLYCcEA5UQM/+xTAm+QCSnuQ== +"@babel/plugin-transform-duplicate-keys@^7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.2.0.tgz#d952c4930f312a4dbfff18f0b2914e60c35530b3" + integrity sha512-q+yuxW4DsTjNceUiTzK0L+AfQ0zD9rWaTLiUqHA8p0gxx7lu1EylenfzjeIWNkPy6e/0VG/Wjw9uf9LueQwLOw== dependencies: "@babel/helper-plugin-utils" "^7.0.0" -"@babel/plugin-transform-exponentiation-operator@^7.1.0": - version "7.1.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.1.0.tgz#9c34c2ee7fd77e02779cfa37e403a2e1003ccc73" - integrity sha512-uZt9kD1Pp/JubkukOGQml9tqAeI8NkE98oZnHZ2qHRElmeKCodbTZgOEUtujSCSLhHSBWbzNiFSDIMC4/RBTLQ== +"@babel/plugin-transform-exponentiation-operator@^7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.2.0.tgz#a63868289e5b4007f7054d46491af51435766008" + integrity sha512-umh4hR6N7mu4Elq9GG8TOu9M0bakvlsREEC+ialrQN6ABS4oDQ69qJv1VtR3uxlKMCQMCvzk7vr17RHKcjx68A== dependencies: "@babel/helper-builder-binary-assignment-operator-visitor" "^7.1.0" "@babel/helper-plugin-utils" "^7.0.0" -"@babel/plugin-transform-for-of@^7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.0.0.tgz#f2ba4eadb83bd17dc3c7e9b30f4707365e1c3e39" - integrity sha512-TlxKecN20X2tt2UEr2LNE6aqA0oPeMT1Y3cgz8k4Dn1j5ObT8M3nl9aA37LLklx0PBZKETC9ZAf9n/6SujTuXA== +"@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== dependencies: "@babel/helper-plugin-utils" "^7.0.0" -"@babel/plugin-transform-function-name@^7.1.0": - version "7.1.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.1.0.tgz#29c5550d5c46208e7f730516d41eeddd4affadbb" - integrity sha512-VxOa1TMlFMtqPW2IDYZQaHsFrq/dDoIjgN098NowhexhZcz3UGlvPgZXuE1jEvNygyWyxRacqDpCZt+par1FNg== +"@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== dependencies: "@babel/helper-function-name" "^7.1.0" "@babel/helper-plugin-utils" "^7.0.0" -"@babel/plugin-transform-literals@^7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.0.0.tgz#2aec1d29cdd24c407359c930cdd89e914ee8ff86" - integrity sha512-1NTDBWkeNXgpUcyoVFxbr9hS57EpZYXpje92zv0SUzjdu3enaRwF/l3cmyRnXLtIdyJASyiS6PtybK+CgKf7jA== +"@babel/plugin-transform-literals@^7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.2.0.tgz#690353e81f9267dad4fd8cfd77eafa86aba53ea1" + integrity sha512-2ThDhm4lI4oV7fVQ6pNNK+sx+c/GM5/SaML0w/r4ZB7sAneD/piDJtwdKlNckXeyGK7wlwg2E2w33C/Hh+VFCg== dependencies: "@babel/helper-plugin-utils" "^7.0.0" -"@babel/plugin-transform-modules-amd@^7.1.0": - version "7.1.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.1.0.tgz#f9e0a7072c12e296079b5a59f408ff5b97bf86a8" - integrity sha512-wt8P+xQ85rrnGNr2x1iV3DW32W8zrB6ctuBkYBbf5/ZzJY99Ob4MFgsZDFgczNU76iy9PWsy4EuxOliDjdKw6A== +"@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" + integrity sha512-mK2A8ucqz1qhrdqjS9VMIDfIvvT2thrEsIQzbaTdc5QFzhDjQv2CkJJ5f6BXIkgbmaoax3zBr2RyvV/8zeoUZw== dependencies: "@babel/helper-module-transforms" "^7.1.0" "@babel/helper-plugin-utils" "^7.0.0" -"@babel/plugin-transform-modules-commonjs@^7.1.0": - version "7.1.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.1.0.tgz#0a9d86451cbbfb29bd15186306897c67f6f9a05c" - integrity sha512-wtNwtMjn1XGwM0AXPspQgvmE6msSJP15CX2RVfpTSTNPLhKhaOjaIfBaVfj4iUZ/VrFSodcFedwtPg/NxwQlPA== +"@babel/plugin-transform-modules-commonjs@^7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.2.0.tgz#c4f1933f5991d5145e9cfad1dfd848ea1727f404" + integrity sha512-V6y0uaUQrQPXUrmj+hgnks8va2L0zcZymeU7TtWEgdRLNkceafKXEduv7QzgQAE4lT+suwooG9dC7LFhdRAbVQ== dependencies: "@babel/helper-module-transforms" "^7.1.0" "@babel/helper-plugin-utils" "^7.0.0" "@babel/helper-simple-access" "^7.1.0" -"@babel/plugin-transform-modules-systemjs@^7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.0.0.tgz#8873d876d4fee23209decc4d1feab8f198cf2df4" - integrity sha512-8EDKMAsitLkiF/D4Zhe9CHEE2XLh4bfLbb9/Zf3FgXYQOZyZYyg7EAel/aT2A7bHv62jwHf09q2KU/oEexr83g== +"@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== dependencies: "@babel/helper-hoist-variables" "^7.0.0" "@babel/helper-plugin-utils" "^7.0.0" -"@babel/plugin-transform-modules-umd@^7.1.0": - version "7.1.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.1.0.tgz#a29a7d85d6f28c3561c33964442257cc6a21f2a8" - integrity sha512-enrRtn5TfRhMmbRwm7F8qOj0qEYByqUvTttPEGimcBH4CJHphjyK1Vg7sdU7JjeEmgSpM890IT/efS2nMHwYig== +"@babel/plugin-transform-modules-umd@^7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.2.0.tgz#7678ce75169f0877b8eb2235538c074268dd01ae" + integrity sha512-BV3bw6MyUH1iIsGhXlOK6sXhmSarZjtJ/vMiD9dNmpY8QXFFQTj+6v92pcfy1iqa8DeAfJFwoxcrS/TUZda6sw== dependencies: "@babel/helper-module-transforms" "^7.1.0" "@babel/helper-plugin-utils" "^7.0.0" @@ -466,18 +474,18 @@ dependencies: "@babel/helper-plugin-utils" "^7.0.0" -"@babel/plugin-transform-object-super@^7.1.0": - version "7.1.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.1.0.tgz#b1ae194a054b826d8d4ba7ca91486d4ada0f91bb" - integrity sha512-/O02Je1CRTSk2SSJaq0xjwQ8hG4zhZGNjE8psTsSNPXyLRCODv7/PBozqT5AmQMzp7MI3ndvMhGdqp9c96tTEw== +"@babel/plugin-transform-object-super@^7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.2.0.tgz#b35d4c10f56bab5d650047dad0f1d8e8814b6598" + integrity sha512-VMyhPYZISFZAqAPVkiYb7dUe2AsVi2/wCT5+wZdsNO31FojQJa9ns40hzZ6U9f50Jlq4w6qwzdBB2uwqZ00ebg== dependencies: "@babel/helper-plugin-utils" "^7.0.0" "@babel/helper-replace-supers" "^7.1.0" -"@babel/plugin-transform-parameters@^7.1.0": - version "7.1.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.1.0.tgz#44f492f9d618c9124026e62301c296bf606a7aed" - integrity sha512-vHV7oxkEJ8IHxTfRr3hNGzV446GAb+0hgbA7o/0Jd76s+YzccdWuTU296FOCOl/xweU4t/Ya4g41yWz80RFCRw== +"@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== dependencies: "@babel/helper-call-delegate" "^7.1.0" "@babel/helper-get-function-arity" "^7.0.0" @@ -490,127 +498,127 @@ dependencies: regenerator-transform "^0.13.3" -"@babel/plugin-transform-shorthand-properties@^7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.0.0.tgz#85f8af592dcc07647541a0350e8c95c7bf419d15" - integrity sha512-g/99LI4vm5iOf5r1Gdxq5Xmu91zvjhEG5+yZDJW268AZELAu4J1EiFLnkSG3yuUsZyOipVOVUKoGPYwfsTymhw== +"@babel/plugin-transform-shorthand-properties@^7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.2.0.tgz#6333aee2f8d6ee7e28615457298934a3b46198f0" + integrity sha512-QP4eUM83ha9zmYtpbnyjTLAGKQritA5XW/iG9cjtuOI8s1RuL/3V6a3DeSHfKutJQ+ayUfeZJPcnCYEQzaPQqg== dependencies: "@babel/helper-plugin-utils" "^7.0.0" -"@babel/plugin-transform-spread@^7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.0.0.tgz#93583ce48dd8c85e53f3a46056c856e4af30b49b" - integrity sha512-L702YFy2EvirrR4shTj0g2xQp7aNwZoWNCkNu2mcoU0uyzMl0XRwDSwzB/xp6DSUFiBmEXuyAyEN16LsgVqGGQ== +"@babel/plugin-transform-spread@^7.2.0": + version "7.2.2" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.2.2.tgz#3103a9abe22f742b6d406ecd3cd49b774919b406" + integrity sha512-KWfky/58vubwtS0hLqEnrWJjsMGaOeSBn90Ezn5Jeg9Z8KKHmELbP1yGylMlm5N6TPKeY9A2+UaSYLdxahg01w== dependencies: "@babel/helper-plugin-utils" "^7.0.0" -"@babel/plugin-transform-sticky-regex@^7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.0.0.tgz#30a9d64ac2ab46eec087b8530535becd90e73366" - integrity sha512-LFUToxiyS/WD+XEWpkx/XJBrUXKewSZpzX68s+yEOtIbdnsRjpryDw9U06gYc6klYEij/+KQVRnD3nz3AoKmjw== +"@babel/plugin-transform-sticky-regex@^7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.2.0.tgz#a1e454b5995560a9c1e0d537dfc15061fd2687e1" + integrity sha512-KKYCoGaRAf+ckH8gEL3JHUaFVyNHKe3ASNsZ+AlktgHevvxGigoIttrEJb8iKN03Q7Eazlv1s6cx2B2cQ3Jabw== dependencies: "@babel/helper-plugin-utils" "^7.0.0" "@babel/helper-regex" "^7.0.0" -"@babel/plugin-transform-template-literals@^7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.0.0.tgz#084f1952efe5b153ddae69eb8945f882c7a97c65" - integrity sha512-vA6rkTCabRZu7Nbl9DfLZE1imj4tzdWcg5vtdQGvj+OH9itNNB6hxuRMHuIY8SGnEt1T9g5foqs9LnrHzsqEFg== +"@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== dependencies: "@babel/helper-annotate-as-pure" "^7.0.0" "@babel/helper-plugin-utils" "^7.0.0" -"@babel/plugin-transform-typeof-symbol@^7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.0.0.tgz#4dcf1e52e943e5267b7313bff347fdbe0f81cec9" - integrity sha512-1r1X5DO78WnaAIvs5uC48t41LLckxsYklJrZjNKcevyz83sF2l4RHbw29qrCPr/6ksFsdfRpT/ZgxNWHXRnffg== +"@babel/plugin-transform-typeof-symbol@^7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.2.0.tgz#117d2bcec2fbf64b4b59d1f9819894682d29f2b2" + integrity sha512-2LNhETWYxiYysBtrBTqL8+La0jIoQQnIScUJc74OYvUGRmkskNY4EzLCnjHBzdmb38wqtTaixpo1NctEcvMDZw== dependencies: "@babel/helper-plugin-utils" "^7.0.0" -"@babel/plugin-transform-unicode-regex@^7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.0.0.tgz#c6780e5b1863a76fe792d90eded9fcd5b51d68fc" - integrity sha512-uJBrJhBOEa3D033P95nPHu3nbFwFE9ZgXsfEitzoIXIwqAZWk7uXcg06yFKXz9FSxBH5ucgU/cYdX0IV8ldHKw== +"@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== dependencies: "@babel/helper-plugin-utils" "^7.0.0" "@babel/helper-regex" "^7.0.0" regexpu-core "^4.1.3" -"@babel/preset-env@^7.1.0": - version "7.1.0" - resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.1.0.tgz#e67ea5b0441cfeab1d6f41e9b5c79798800e8d11" - integrity sha512-ZLVSynfAoDHB/34A17/JCZbyrzbQj59QC1Anyueb4Bwjh373nVPq5/HMph0z+tCmcDjXDe+DlKQq9ywQuvWrQg== +"@babel/preset-env@^7.2.3": + version "7.2.3" + resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.2.3.tgz#948c8df4d4609c99c7e0130169f052ea6a7a8933" + integrity sha512-AuHzW7a9rbv5WXmvGaPX7wADxFkZIqKlbBh1dmZUQp4iwiPpkE/Qnrji6SC4UQCQzvWY/cpHET29eUhXS9cLPw== dependencies: "@babel/helper-module-imports" "^7.0.0" "@babel/helper-plugin-utils" "^7.0.0" - "@babel/plugin-proposal-async-generator-functions" "^7.1.0" - "@babel/plugin-proposal-json-strings" "^7.0.0" - "@babel/plugin-proposal-object-rest-spread" "^7.0.0" - "@babel/plugin-proposal-optional-catch-binding" "^7.0.0" - "@babel/plugin-proposal-unicode-property-regex" "^7.0.0" - "@babel/plugin-syntax-async-generators" "^7.0.0" - "@babel/plugin-syntax-object-rest-spread" "^7.0.0" - "@babel/plugin-syntax-optional-catch-binding" "^7.0.0" - "@babel/plugin-transform-arrow-functions" "^7.0.0" - "@babel/plugin-transform-async-to-generator" "^7.1.0" - "@babel/plugin-transform-block-scoped-functions" "^7.0.0" - "@babel/plugin-transform-block-scoping" "^7.0.0" - "@babel/plugin-transform-classes" "^7.1.0" - "@babel/plugin-transform-computed-properties" "^7.0.0" - "@babel/plugin-transform-destructuring" "^7.0.0" - "@babel/plugin-transform-dotall-regex" "^7.0.0" - "@babel/plugin-transform-duplicate-keys" "^7.0.0" - "@babel/plugin-transform-exponentiation-operator" "^7.1.0" - "@babel/plugin-transform-for-of" "^7.0.0" - "@babel/plugin-transform-function-name" "^7.1.0" - "@babel/plugin-transform-literals" "^7.0.0" - "@babel/plugin-transform-modules-amd" "^7.1.0" - "@babel/plugin-transform-modules-commonjs" "^7.1.0" - "@babel/plugin-transform-modules-systemjs" "^7.0.0" - "@babel/plugin-transform-modules-umd" "^7.1.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.2.0" + "@babel/plugin-proposal-optional-catch-binding" "^7.2.0" + "@babel/plugin-proposal-unicode-property-regex" "^7.2.0" + "@babel/plugin-syntax-async-generators" "^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-block-scoped-functions" "^7.2.0" + "@babel/plugin-transform-block-scoping" "^7.2.0" + "@babel/plugin-transform-classes" "^7.2.0" + "@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-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-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-umd" "^7.2.0" "@babel/plugin-transform-new-target" "^7.0.0" - "@babel/plugin-transform-object-super" "^7.1.0" - "@babel/plugin-transform-parameters" "^7.1.0" + "@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-shorthand-properties" "^7.0.0" - "@babel/plugin-transform-spread" "^7.0.0" - "@babel/plugin-transform-sticky-regex" "^7.0.0" - "@babel/plugin-transform-template-literals" "^7.0.0" - "@babel/plugin-transform-typeof-symbol" "^7.0.0" - "@babel/plugin-transform-unicode-regex" "^7.0.0" - browserslist "^4.1.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-typeof-symbol" "^7.2.0" + "@babel/plugin-transform-unicode-regex" "^7.2.0" + browserslist "^4.3.4" invariant "^2.2.2" js-levenshtein "^1.1.3" semver "^5.3.0" -"@babel/template@^7.0.0", "@babel/template@^7.1.0", "@babel/template@^7.1.2": - version "7.1.2" - resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.1.2.tgz#090484a574fef5a2d2d7726a674eceda5c5b5644" - integrity sha512-SY1MmplssORfFiLDcOETrW7fCLl+PavlwMh92rrGcikQaRq4iWPVH0MpwPpY3etVMx6RnDjXtr6VZYr/IbP/Ag== +"@babel/template@^7.0.0", "@babel/template@^7.1.0", "@babel/template@^7.1.2", "@babel/template@^7.2.2": + version "7.2.2" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.2.2.tgz#005b3fdf0ed96e88041330379e0da9a708eb2907" + integrity sha512-zRL0IMM02AUDwghf5LMSSDEz7sBCO2YnNmpg3uWTZj/v1rcG2BmQUvaGU8GhU8BvfMh1k2KIAYZ7Ji9KXPUg7g== dependencies: "@babel/code-frame" "^7.0.0" - "@babel/parser" "^7.1.2" - "@babel/types" "^7.1.2" + "@babel/parser" "^7.2.2" + "@babel/types" "^7.2.2" -"@babel/traverse@^7.0.0", "@babel/traverse@^7.1.0": - version "7.1.0" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.1.0.tgz#503ec6669387efd182c3888c4eec07bcc45d91b2" - integrity sha512-bwgln0FsMoxm3pLOgrrnGaXk18sSM9JNf1/nHC/FksmNGFbYnPWY4GYCfLxyP1KRmfsxqkRpfoa6xr6VuuSxdw== +"@babel/traverse@^7.0.0", "@babel/traverse@^7.1.0", "@babel/traverse@^7.1.5", "@babel/traverse@^7.2.2", "@babel/traverse@^7.2.3": + version "7.2.3" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.2.3.tgz#7ff50cefa9c7c0bd2d81231fdac122f3957748d8" + integrity sha512-Z31oUD/fJvEWVR0lNZtfgvVt512ForCTNKYcJBGbPb1QZfve4WGH8Wsy7+Mev33/45fhP/hwQtvgusNdcCMgSw== dependencies: "@babel/code-frame" "^7.0.0" - "@babel/generator" "^7.0.0" + "@babel/generator" "^7.2.2" "@babel/helper-function-name" "^7.1.0" "@babel/helper-split-export-declaration" "^7.0.0" - "@babel/parser" "^7.1.0" - "@babel/types" "^7.0.0" - debug "^3.1.0" + "@babel/parser" "^7.2.3" + "@babel/types" "^7.2.2" + debug "^4.1.0" globals "^11.1.0" lodash "^4.17.10" -"@babel/types@^7.0.0", "@babel/types@^7.1.2": - version "7.1.2" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.1.2.tgz#183e7952cf6691628afdc2e2b90d03240bac80c0" - integrity sha512-pb1I05sZEKiSlMUV9UReaqsCPUpgbHHHu2n1piRm7JkuBkm6QxcaIzKu6FMnMtCbih/cEYTR+RGYYC96Yk9HAg== +"@babel/types@^7.0.0", "@babel/types@^7.2.0", "@babel/types@^7.2.2": + version "7.2.2" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.2.2.tgz#44e10fc24e33af524488b716cdaee5360ea8ed1e" + integrity sha512-fKCuD6UFUMkR541eDWL+2ih/xFZBXPOg/7EQFeTluMDebfqR4jrpaCjLhkWlQS4hT6nRa2PMEgXKbRB5/H2fpg== dependencies: esutils "^2.0.2" lodash "^4.17.10" @@ -743,151 +751,147 @@ dependencies: lodash "^4.17.4" -"@webassemblyjs/ast@1.7.6": - version "1.7.6" - resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.7.6.tgz#3ef8c45b3e5e943a153a05281317474fef63e21e" - integrity sha512-8nkZS48EVsMUU0v6F1LCIOw4RYWLm2plMtbhFTjNgeXmsTNLuU3xTRtnljt9BFQB+iPbLRobkNrCWftWnNC7wQ== - dependencies: - "@webassemblyjs/helper-module-context" "1.7.6" - "@webassemblyjs/helper-wasm-bytecode" "1.7.6" - "@webassemblyjs/wast-parser" "1.7.6" - mamacro "^0.0.3" - -"@webassemblyjs/floating-point-hex-parser@1.7.6": - version "1.7.6" - resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.7.6.tgz#7cb37d51a05c3fe09b464ae7e711d1ab3837801f" - integrity sha512-VBOZvaOyBSkPZdIt5VBMg3vPWxouuM13dPXGWI1cBh3oFLNcFJ8s9YA7S9l4mPI7+Q950QqOmqj06oa83hNWBA== - -"@webassemblyjs/helper-api-error@1.7.6": - version "1.7.6" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.7.6.tgz#99b7e30e66f550a2638299a109dda84a622070ef" - integrity sha512-SCzhcQWHXfrfMSKcj8zHg1/kL9kb3aa5TN4plc/EREOs5Xop0ci5bdVBApbk2yfVi8aL+Ly4Qpp3/TRAUInjrg== - -"@webassemblyjs/helper-buffer@1.7.6": - version "1.7.6" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.7.6.tgz#ba0648be12bbe560c25c997e175c2018df39ca3e" - integrity sha512-1/gW5NaGsEOZ02fjnFiU8/OEEXU1uVbv2um0pQ9YVL3IHSkyk6xOwokzyqqO1qDZQUAllb+V8irtClPWntbVqw== - -"@webassemblyjs/helper-code-frame@1.7.6": - version "1.7.6" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-code-frame/-/helper-code-frame-1.7.6.tgz#5a94d21b0057b69a7403fca0c253c3aaca95b1a5" - integrity sha512-+suMJOkSn9+vEvDvgyWyrJo5vJsWSDXZmJAjtoUq4zS4eqHyXImpktvHOZwXp1XQjO5H+YQwsBgqTQEc0J/5zg== - dependencies: - "@webassemblyjs/wast-printer" "1.7.6" - -"@webassemblyjs/helper-fsm@1.7.6": - version "1.7.6" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-fsm/-/helper-fsm-1.7.6.tgz#ae1741c6f6121213c7a0b587fb964fac492d3e49" - integrity sha512-HCS6KN3wgxUihGBW7WFzEC/o8Eyvk0d56uazusnxXthDPnkWiMv+kGi9xXswL2cvfYfeK5yiM17z2K5BVlwypw== - -"@webassemblyjs/helper-module-context@1.7.6": - version "1.7.6" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-module-context/-/helper-module-context-1.7.6.tgz#116d19a51a6cebc8900ad53ca34ff8269c668c23" - integrity sha512-e8/6GbY7OjLM+6OsN7f2krC2qYVNaSr0B0oe4lWdmq5sL++8dYDD1TFbD1TdAdWMRTYNr/Qq7ovXWzia2EbSjw== - dependencies: - mamacro "^0.0.3" - -"@webassemblyjs/helper-wasm-bytecode@1.7.6": - version "1.7.6" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.7.6.tgz#98e515eaee611aa6834eb5f6a7f8f5b29fefb6f1" - integrity sha512-PzYFCb7RjjSdAOljyvLWVqd6adAOabJW+8yRT+NWhXuf1nNZWH+igFZCUK9k7Cx7CsBbzIfXjJc7u56zZgFj9Q== - -"@webassemblyjs/helper-wasm-section@1.7.6": - version "1.7.6" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.7.6.tgz#783835867bdd686df7a95377ab64f51a275e8333" - integrity sha512-3GS628ppDPSuwcYlQ7cDCGr4W2n9c4hLzvnRKeuz+lGsJSmc/ADVoYpm1ts2vlB1tGHkjtQMni+yu8mHoMlKlA== - dependencies: - "@webassemblyjs/ast" "1.7.6" - "@webassemblyjs/helper-buffer" "1.7.6" - "@webassemblyjs/helper-wasm-bytecode" "1.7.6" - "@webassemblyjs/wasm-gen" "1.7.6" - -"@webassemblyjs/ieee754@1.7.6": - version "1.7.6" - resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.7.6.tgz#c34fc058f2f831fae0632a8bb9803cf2d3462eb1" - integrity sha512-V4cIp0ruyw+hawUHwQLn6o2mFEw4t50tk530oKsYXQhEzKR+xNGDxs/SFFuyTO7X3NzEu4usA3w5jzhl2RYyzQ== +"@webassemblyjs/ast@1.7.11": + version "1.7.11" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.7.11.tgz#b988582cafbb2b095e8b556526f30c90d057cace" + integrity sha512-ZEzy4vjvTzScC+SH8RBssQUawpaInUdMTYwYYLh54/s8TuT0gBLuyUnppKsVyZEi876VmmStKsUs28UxPgdvrA== + dependencies: + "@webassemblyjs/helper-module-context" "1.7.11" + "@webassemblyjs/helper-wasm-bytecode" "1.7.11" + "@webassemblyjs/wast-parser" "1.7.11" + +"@webassemblyjs/floating-point-hex-parser@1.7.11": + version "1.7.11" + resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.7.11.tgz#a69f0af6502eb9a3c045555b1a6129d3d3f2e313" + integrity sha512-zY8dSNyYcgzNRNT666/zOoAyImshm3ycKdoLsyDw/Bwo6+/uktb7p4xyApuef1dwEBo/U/SYQzbGBvV+nru2Xg== + +"@webassemblyjs/helper-api-error@1.7.11": + version "1.7.11" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.7.11.tgz#c7b6bb8105f84039511a2b39ce494f193818a32a" + integrity sha512-7r1qXLmiglC+wPNkGuXCvkmalyEstKVwcueZRP2GNC2PAvxbLYwLLPr14rcdJaE4UtHxQKfFkuDFuv91ipqvXg== + +"@webassemblyjs/helper-buffer@1.7.11": + version "1.7.11" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.7.11.tgz#3122d48dcc6c9456ed982debe16c8f37101df39b" + integrity sha512-MynuervdylPPh3ix+mKZloTcL06P8tenNH3sx6s0qE8SLR6DdwnfgA7Hc9NSYeob2jrW5Vql6GVlsQzKQCa13w== + +"@webassemblyjs/helper-code-frame@1.7.11": + version "1.7.11" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-code-frame/-/helper-code-frame-1.7.11.tgz#cf8f106e746662a0da29bdef635fcd3d1248364b" + integrity sha512-T8ESC9KMXFTXA5urJcyor5cn6qWeZ4/zLPyWeEXZ03hj/x9weSokGNkVCdnhSabKGYWxElSdgJ+sFa9G/RdHNw== + dependencies: + "@webassemblyjs/wast-printer" "1.7.11" + +"@webassemblyjs/helper-fsm@1.7.11": + version "1.7.11" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-fsm/-/helper-fsm-1.7.11.tgz#df38882a624080d03f7503f93e3f17ac5ac01181" + integrity sha512-nsAQWNP1+8Z6tkzdYlXT0kxfa2Z1tRTARd8wYnc/e3Zv3VydVVnaeePgqUzFrpkGUyhUUxOl5ML7f1NuT+gC0A== + +"@webassemblyjs/helper-module-context@1.7.11": + version "1.7.11" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-module-context/-/helper-module-context-1.7.11.tgz#d874d722e51e62ac202476935d649c802fa0e209" + integrity sha512-JxfD5DX8Ygq4PvXDucq0M+sbUFA7BJAv/GGl9ITovqE+idGX+J3QSzJYz+LwQmL7fC3Rs+utvWoJxDb6pmC0qg== + +"@webassemblyjs/helper-wasm-bytecode@1.7.11": + version "1.7.11" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.7.11.tgz#dd9a1e817f1c2eb105b4cf1013093cb9f3c9cb06" + integrity sha512-cMXeVS9rhoXsI9LLL4tJxBgVD/KMOKXuFqYb5oCJ/opScWpkCMEz9EJtkonaNcnLv2R3K5jIeS4TRj/drde1JQ== + +"@webassemblyjs/helper-wasm-section@1.7.11": + version "1.7.11" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.7.11.tgz#9c9ac41ecf9fbcfffc96f6d2675e2de33811e68a" + integrity sha512-8ZRY5iZbZdtNFE5UFunB8mmBEAbSI3guwbrsCl4fWdfRiAcvqQpeqd5KHhSWLL5wuxo53zcaGZDBU64qgn4I4Q== + dependencies: + "@webassemblyjs/ast" "1.7.11" + "@webassemblyjs/helper-buffer" "1.7.11" + "@webassemblyjs/helper-wasm-bytecode" "1.7.11" + "@webassemblyjs/wasm-gen" "1.7.11" + +"@webassemblyjs/ieee754@1.7.11": + version "1.7.11" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.7.11.tgz#c95839eb63757a31880aaec7b6512d4191ac640b" + integrity sha512-Mmqx/cS68K1tSrvRLtaV/Lp3NZWzXtOHUW2IvDvl2sihAwJh4ACE0eL6A8FvMyDG9abes3saB6dMimLOs+HMoQ== dependencies: "@xtuc/ieee754" "^1.2.0" -"@webassemblyjs/leb128@1.7.6": - version "1.7.6" - resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.7.6.tgz#197f75376a29f6ed6ace15898a310d871d92f03b" - integrity sha512-ojdlG8WpM394lBow4ncTGJoIVZ4aAtNOWHhfAM7m7zprmkVcKK+2kK5YJ9Bmj6/ketTtOn7wGSHCtMt+LzqgYQ== +"@webassemblyjs/leb128@1.7.11": + version "1.7.11" + resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.7.11.tgz#d7267a1ee9c4594fd3f7e37298818ec65687db63" + integrity sha512-vuGmgZjjp3zjcerQg+JA+tGOncOnJLWVkt8Aze5eWQLwTQGNgVLcyOTqgSCxWTR4J42ijHbBxnuRaL1Rv7XMdw== dependencies: "@xtuc/long" "4.2.1" -"@webassemblyjs/utf8@1.7.6": - version "1.7.6" - resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.7.6.tgz#eb62c66f906af2be70de0302e29055d25188797d" - integrity sha512-oId+tLxQ+AeDC34ELRYNSqJRaScB0TClUU6KQfpB8rNT6oelYlz8axsPhf6yPTg7PBJ/Z5WcXmUYiHEWgbbHJw== - -"@webassemblyjs/wasm-edit@1.7.6": - version "1.7.6" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.7.6.tgz#fa41929160cd7d676d4c28ecef420eed5b3733c5" - integrity sha512-pTNjLO3o41v/Vz9VFLl+I3YLImpCSpodFW77pNoH4agn5I6GgSxXHXtvWDTvYJFty0jSeXZWLEmbaSIRUDlekg== - dependencies: - "@webassemblyjs/ast" "1.7.6" - "@webassemblyjs/helper-buffer" "1.7.6" - "@webassemblyjs/helper-wasm-bytecode" "1.7.6" - "@webassemblyjs/helper-wasm-section" "1.7.6" - "@webassemblyjs/wasm-gen" "1.7.6" - "@webassemblyjs/wasm-opt" "1.7.6" - "@webassemblyjs/wasm-parser" "1.7.6" - "@webassemblyjs/wast-printer" "1.7.6" - -"@webassemblyjs/wasm-gen@1.7.6": - version "1.7.6" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.7.6.tgz#695ac38861ab3d72bf763c8c75e5f087ffabc322" - integrity sha512-mQvFJVumtmRKEUXMohwn8nSrtjJJl6oXwF3FotC5t6e2hlKMh8sIaW03Sck2MDzw9xPogZD7tdP5kjPlbH9EcQ== - dependencies: - "@webassemblyjs/ast" "1.7.6" - "@webassemblyjs/helper-wasm-bytecode" "1.7.6" - "@webassemblyjs/ieee754" "1.7.6" - "@webassemblyjs/leb128" "1.7.6" - "@webassemblyjs/utf8" "1.7.6" - -"@webassemblyjs/wasm-opt@1.7.6": - version "1.7.6" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.7.6.tgz#fbafa78e27e1a75ab759a4b658ff3d50b4636c21" - integrity sha512-go44K90fSIsDwRgtHhX14VtbdDPdK2sZQtZqUcMRvTojdozj5tLI0VVJAzLCfz51NOkFXezPeVTAYFqrZ6rI8Q== - dependencies: - "@webassemblyjs/ast" "1.7.6" - "@webassemblyjs/helper-buffer" "1.7.6" - "@webassemblyjs/wasm-gen" "1.7.6" - "@webassemblyjs/wasm-parser" "1.7.6" - -"@webassemblyjs/wasm-parser@1.7.6": - version "1.7.6" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.7.6.tgz#84eafeeff405ad6f4c4b5777d6a28ae54eed51fe" - integrity sha512-t1T6TfwNY85pDA/HWPA8kB9xA4sp9ajlRg5W7EKikqrynTyFo+/qDzIpvdkOkOGjlS6d4n4SX59SPuIayR22Yg== - dependencies: - "@webassemblyjs/ast" "1.7.6" - "@webassemblyjs/helper-api-error" "1.7.6" - "@webassemblyjs/helper-wasm-bytecode" "1.7.6" - "@webassemblyjs/ieee754" "1.7.6" - "@webassemblyjs/leb128" "1.7.6" - "@webassemblyjs/utf8" "1.7.6" - -"@webassemblyjs/wast-parser@1.7.6": - version "1.7.6" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-parser/-/wast-parser-1.7.6.tgz#ca4d20b1516e017c91981773bd7e819d6bd9c6a7" - integrity sha512-1MaWTErN0ziOsNUlLdvwS+NS1QWuI/kgJaAGAMHX8+fMJFgOJDmN/xsG4h/A1Gtf/tz5VyXQciaqHZqp2q0vfg== - dependencies: - "@webassemblyjs/ast" "1.7.6" - "@webassemblyjs/floating-point-hex-parser" "1.7.6" - "@webassemblyjs/helper-api-error" "1.7.6" - "@webassemblyjs/helper-code-frame" "1.7.6" - "@webassemblyjs/helper-fsm" "1.7.6" +"@webassemblyjs/utf8@1.7.11": + version "1.7.11" + resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.7.11.tgz#06d7218ea9fdc94a6793aa92208160db3d26ee82" + integrity sha512-C6GFkc7aErQIAH+BMrIdVSmW+6HSe20wg57HEC1uqJP8E/xpMjXqQUxkQw07MhNDSDcGpxI9G5JSNOQCqJk4sA== + +"@webassemblyjs/wasm-edit@1.7.11": + version "1.7.11" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.7.11.tgz#8c74ca474d4f951d01dbae9bd70814ee22a82005" + integrity sha512-FUd97guNGsCZQgeTPKdgxJhBXkUbMTY6hFPf2Y4OedXd48H97J+sOY2Ltaq6WGVpIH8o/TGOVNiVz/SbpEMJGg== + dependencies: + "@webassemblyjs/ast" "1.7.11" + "@webassemblyjs/helper-buffer" "1.7.11" + "@webassemblyjs/helper-wasm-bytecode" "1.7.11" + "@webassemblyjs/helper-wasm-section" "1.7.11" + "@webassemblyjs/wasm-gen" "1.7.11" + "@webassemblyjs/wasm-opt" "1.7.11" + "@webassemblyjs/wasm-parser" "1.7.11" + "@webassemblyjs/wast-printer" "1.7.11" + +"@webassemblyjs/wasm-gen@1.7.11": + version "1.7.11" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.7.11.tgz#9bbba942f22375686a6fb759afcd7ac9c45da1a8" + integrity sha512-U/KDYp7fgAZX5KPfq4NOupK/BmhDc5Kjy2GIqstMhvvdJRcER/kUsMThpWeRP8BMn4LXaKhSTggIJPOeYHwISA== + dependencies: + "@webassemblyjs/ast" "1.7.11" + "@webassemblyjs/helper-wasm-bytecode" "1.7.11" + "@webassemblyjs/ieee754" "1.7.11" + "@webassemblyjs/leb128" "1.7.11" + "@webassemblyjs/utf8" "1.7.11" + +"@webassemblyjs/wasm-opt@1.7.11": + version "1.7.11" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.7.11.tgz#b331e8e7cef8f8e2f007d42c3a36a0580a7d6ca7" + integrity sha512-XynkOwQyiRidh0GLua7SkeHvAPXQV/RxsUeERILmAInZegApOUAIJfRuPYe2F7RcjOC9tW3Cb9juPvAC/sCqvg== + dependencies: + "@webassemblyjs/ast" "1.7.11" + "@webassemblyjs/helper-buffer" "1.7.11" + "@webassemblyjs/wasm-gen" "1.7.11" + "@webassemblyjs/wasm-parser" "1.7.11" + +"@webassemblyjs/wasm-parser@1.7.11": + version "1.7.11" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.7.11.tgz#6e3d20fa6a3519f6b084ef9391ad58211efb0a1a" + integrity sha512-6lmXRTrrZjYD8Ng8xRyvyXQJYUQKYSXhJqXOBLw24rdiXsHAOlvw5PhesjdcaMadU/pyPQOJ5dHreMjBxwnQKg== + dependencies: + "@webassemblyjs/ast" "1.7.11" + "@webassemblyjs/helper-api-error" "1.7.11" + "@webassemblyjs/helper-wasm-bytecode" "1.7.11" + "@webassemblyjs/ieee754" "1.7.11" + "@webassemblyjs/leb128" "1.7.11" + "@webassemblyjs/utf8" "1.7.11" + +"@webassemblyjs/wast-parser@1.7.11": + version "1.7.11" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-parser/-/wast-parser-1.7.11.tgz#25bd117562ca8c002720ff8116ef9072d9ca869c" + integrity sha512-lEyVCg2np15tS+dm7+JJTNhNWq9yTZvi3qEhAIIOaofcYlUp0UR5/tVqOwa/gXYr3gjwSZqw+/lS9dscyLelbQ== + dependencies: + "@webassemblyjs/ast" "1.7.11" + "@webassemblyjs/floating-point-hex-parser" "1.7.11" + "@webassemblyjs/helper-api-error" "1.7.11" + "@webassemblyjs/helper-code-frame" "1.7.11" + "@webassemblyjs/helper-fsm" "1.7.11" "@xtuc/long" "4.2.1" - mamacro "^0.0.3" -"@webassemblyjs/wast-printer@1.7.6": - version "1.7.6" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.7.6.tgz#a6002c526ac5fa230fe2c6d2f1bdbf4aead43a5e" - integrity sha512-vHdHSK1tOetvDcl1IV1OdDeGNe/NDDQ+KzuZHMtqTVP1xO/tZ/IKNpj5BaGk1OYFdsDWQqb31PIwdEyPntOWRQ== +"@webassemblyjs/wast-printer@1.7.11": + version "1.7.11" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.7.11.tgz#c4245b6de242cb50a2cc950174fdbf65c78d7813" + integrity sha512-m5vkAsuJ32QpkdkDOUPGSltrg8Cuk3KBx4YrmAGQwCZPRdUHXxG4phIOuuycLemHFr74sWL9Wthqss4fzdzSwg== dependencies: - "@webassemblyjs/ast" "1.7.6" - "@webassemblyjs/wast-parser" "1.7.6" + "@webassemblyjs/ast" "1.7.11" + "@webassemblyjs/wast-parser" "1.7.11" "@xtuc/long" "4.2.1" "@xtuc/ieee754@^1.2.0": @@ -1436,12 +1440,12 @@ babel-jest@^23.6.0: babel-plugin-istanbul "^4.1.6" babel-preset-jest "^23.2.0" -babel-loader@^8.0.4: - version "8.0.4" - resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-8.0.4.tgz#7bbf20cbe4560629e2e41534147692d3fecbdce6" - integrity sha512-fhBhNkUToJcW9nV46v8w87AJOwAJDz84c1CL57n3Stj73FANM/b9TbCUK4YhdOwEyZ+OxhYpdeZDNzSI29Firw== +babel-loader@^8.0.5: + version "8.0.5" + resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-8.0.5.tgz#225322d7509c2157655840bba52e46b6c2f2fe33" + integrity sha512-NTnHnVRd2JnRqPC0vW+iOQWU5pchDbYXsG2E6DMXEpMfUcQKclF9gmf3G3ZMhzG7IG9ji4coL0cm+FxeWxDpnw== dependencies: - find-cache-dir "^1.0.0" + find-cache-dir "^2.0.0" loader-utils "^1.0.2" mkdirp "^0.5.1" util.promisify "^1.0.0" @@ -1591,6 +1595,11 @@ babel-types@^6.0.0, babel-types@^6.18.0, babel-types@^6.24.1, babel-types@^6.26. lodash "^4.17.4" to-fast-properties "^1.0.3" +babylon@7.0.0-beta.19: + version "7.0.0-beta.19" + resolved "https://registry.yarnpkg.com/babylon/-/babylon-7.0.0-beta.19.tgz#e928c7e807e970e0536b078ab3e0c48f9e052503" + integrity sha512-Vg0C9s/REX6/WIXN37UKpv5ZhRi6A4pjHlpkE34+8/a6c2W1Q692n3hmc+SZG5lKRnaExLUbxtJ1SVT+KaCQ/A== + babylon@^6.18.0: version "6.18.0" resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.18.0.tgz#af2f3b88fa6f5c1e4c634d1a0f8eac4f55b395e3" @@ -1683,10 +1692,10 @@ blob@0.0.4: resolved "https://registry.yarnpkg.com/blob/-/blob-0.0.4.tgz#bcf13052ca54463f30f9fc7e95b9a47630a94921" integrity sha1-vPEwUspURj8w+fx+lbmkdjCpSSE= -bluebird@^3.1.1, bluebird@^3.3.0, bluebird@^3.5.1: - version "3.5.1" - resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.1.tgz#d9551f9de98f1fcda1e683d17ee91a0602ee2eb9" - integrity sha512-MKiLiV+I1AA596t9w1sQJ8jkiSr5+ZKi0WKrYGUn6d1Fx+Ij4tIj+m2WMQSGczs5jZVxV339chE8iwk6F64wjA== +bluebird@^3.1.1, bluebird@^3.3.0, bluebird@^3.5.1, bluebird@^3.5.3, bluebird@~3.5.0: + version "3.5.3" + resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.3.tgz#7d01c6f9616c9a51ab0f8c549a79dfe6ec33efa7" + integrity sha512-/qKPUQlaW1OyR51WeCPBvRnAlnZFUJkCSG5HzGnuIqhgyJtF+T94lFnn33eiazjRm2LAHVy2guNnaq48X9SJuw== bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.4.0: version "4.11.8" @@ -1868,14 +1877,14 @@ browserify-zlib@^0.2.0: dependencies: pako "~1.0.5" -browserslist@^4.1.0: - version "4.1.1" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.1.1.tgz#328eb4ff1215b12df6589e9ab82f8adaa4fc8cd6" - integrity sha512-VBorw+tgpOtZ1BYhrVSVTzTt/3+vSE3eFUh0N2GCFK1HffceOaf32YS/bs6WiFhjDAblAFrx85jMy3BG9fBK2Q== +browserslist@^4.3.4: + version "4.3.7" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.3.7.tgz#f1de479a6466ea47a0a26dcc725e7504817e624a" + integrity sha512-pWQv51Ynb0MNk9JGMCZ8VkM785/4MQNXiFYtPqI7EEP0TJO+/d/NqRVn1uiAN0DNbnlUSpL2sh16Kspasv3pUQ== dependencies: - caniuse-lite "^1.0.30000884" - electron-to-chromium "^1.3.62" - node-releases "^1.0.0-alpha.11" + caniuse-lite "^1.0.30000925" + electron-to-chromium "^1.3.96" + node-releases "^1.1.3" bser@^2.0.0: version "2.0.0" @@ -1923,43 +1932,24 @@ bytes@3.0.0: resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" integrity sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg= -cacache@^10.0.4: - version "10.0.4" - resolved "https://registry.yarnpkg.com/cacache/-/cacache-10.0.4.tgz#6452367999eff9d4188aefd9a14e9d7c6a263460" - integrity sha512-Dph0MzuH+rTQzGPNT9fAnrPmMmjKfST6trxJeK7NQuHRaVw24VzPRWTmg9MpcwOVQZO0E1FBICUlFeNaKPIfHA== +cacache@^11.0.2, cacache@^11.2.0: + version "11.3.2" + resolved "https://registry.yarnpkg.com/cacache/-/cacache-11.3.2.tgz#2d81e308e3d258ca38125b676b98b2ac9ce69bfa" + integrity sha512-E0zP4EPGDOaT2chM08Als91eYnf8Z+eH1awwwVsngUmgppfM5jjJ8l3z5vO5p5w/I3LsiXawb1sW0VY65pQABg== dependencies: - bluebird "^3.5.1" - chownr "^1.0.1" - glob "^7.1.2" - graceful-fs "^4.1.11" - lru-cache "^4.1.1" - mississippi "^2.0.0" - mkdirp "^0.5.1" - move-concurrently "^1.0.1" - promise-inflight "^1.0.1" - rimraf "^2.6.2" - ssri "^5.2.4" - unique-filename "^1.1.0" - y18n "^4.0.0" - -cacache@^11.2.0: - version "11.2.0" - resolved "https://registry.yarnpkg.com/cacache/-/cacache-11.2.0.tgz#617bdc0b02844af56310e411c0878941d5739965" - integrity sha512-IFWl6lfK6wSeYCHUXh+N1lY72UDrpyrYQJNIVQf48paDuWbv5RbAtJYf/4gUQFObTCHZwdZ5sI8Iw7nqwP6nlQ== - dependencies: - bluebird "^3.5.1" - chownr "^1.0.1" - figgy-pudding "^3.1.0" - glob "^7.1.2" - graceful-fs "^4.1.11" - lru-cache "^4.1.3" + bluebird "^3.5.3" + chownr "^1.1.1" + figgy-pudding "^3.5.1" + glob "^7.1.3" + graceful-fs "^4.1.15" + lru-cache "^5.1.1" mississippi "^3.0.0" mkdirp "^0.5.1" move-concurrently "^1.0.1" promise-inflight "^1.0.1" rimraf "^2.6.2" - ssri "^6.0.0" - unique-filename "^1.1.0" + ssri "^6.0.1" + unique-filename "^1.1.1" y18n "^4.0.0" cache-base@^1.0.1: @@ -1977,15 +1967,16 @@ cache-base@^1.0.1: union-value "^1.0.0" unset-value "^1.0.0" -cache-loader@^1.2.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/cache-loader/-/cache-loader-1.2.2.tgz#6d5c38ded959a09cc5d58190ab5af6f73bd353f5" - integrity sha512-rsGh4SIYyB9glU+d0OcHwiXHXBoUgDhHZaQ1KAbiXqfz1CDPxtTboh1gPbJ0q2qdO8a9lfcjgC5CJ2Ms32y5bw== +cache-loader@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/cache-loader/-/cache-loader-2.0.1.tgz#5758f41a62d7c23941e3c3c7016e6faeb03acb07" + integrity sha512-V99T3FOynmGx26Zom+JrVBytLBsmUCzVG2/4NnUKgvXN4bEV42R1ERl1IyiH/cvFIDA1Ytq2lPZ9tXDSahcQpQ== dependencies: loader-utils "^1.1.0" mkdirp "^0.5.1" - neo-async "^2.5.0" - schema-utils "^0.4.2" + neo-async "^2.6.0" + normalize-path "^3.0.0" + schema-utils "^1.0.0" cacheable-request@^2.1.1: version "2.1.4" @@ -2027,10 +2018,15 @@ camelcase@^4.0.0, camelcase@^4.1.0: resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-4.1.0.tgz#d545635be1e33c542649c69173e5de6acfae34dd" integrity sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0= -caniuse-lite@^1.0.30000884: - version "1.0.30000888" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000888.tgz#22edb50d91dd70612b5898e3b36f460600c6492f" - integrity sha512-vftg+5p/lPsQGpnhSo/yBuYL36ai/cyjLvU3dOPJY1kkKrekLWIy8SLm+wzjX0hpCUdFTasC4/ZT7uqw4rKOnQ== +camelcase@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.0.0.tgz#03295527d58bd3cd4aa75363f35b2e8d97be2f42" + integrity sha512-faqwZqnWxbxn+F1d399ygeamQNy3lPp/H9H6rNrqYh4FSVCtcY+3cub1MxA8o9mDd55mM8Aghuu/kuyYA6VTsA== + +caniuse-lite@^1.0.30000925: + version "1.0.30000927" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000927.tgz#114a9de4ff1e01f5790fe578ecd93421c7524665" + integrity sha512-ogq4NbUWf1uG/j66k0AmiO3GjqJAlQyF8n4w8a954cbCyFKmYGvRtgz6qkq2fWuduTXHibX7GyYL5Pg58Aks2g== capture-exit@^1.2.0: version "1.2.0" @@ -2049,6 +2045,13 @@ caseless@~0.12.0: resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw= +catharsis@~0.8.9: + version "0.8.9" + resolved "https://registry.yarnpkg.com/catharsis/-/catharsis-0.8.9.tgz#98cc890ca652dd2ef0e70b37925310ff9e90fc8b" + integrity sha1-mMyJDKZS3S7w5ws3klMQ/56Q/Is= + dependencies: + underscore-contrib "~0.3.0" + chalk@1.1.3, chalk@^1.0.0, chalk@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" @@ -2114,10 +2117,10 @@ chokidar@^2.0.0, chokidar@^2.0.2, chokidar@^2.0.3: optionalDependencies: fsevents "^1.2.2" -chownr@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.0.1.tgz#e2a75042a9551908bebd25b8523d5f9769d79181" - integrity sha1-4qdQQqlVGQi+vSW4Uj1fl2nXkYE= +chownr@^1.0.1, chownr@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.1.tgz#54726b8b8fff4df053c42187e801fb4412df1494" + integrity sha512-j38EvO5+LHX84jlo6h4UzmOwi0UgW61WRyPtJz4qaadK5eY3BTS5TY/S1Stc3Uk2lIM6TPevAlULiEJwie860g== chrome-trace-event@^1.0.0: version "1.0.0" @@ -2284,11 +2287,6 @@ commander@2, commander@^2.10.0, commander@^2.18.0, commander@^2.19.0: resolved "https://registry.yarnpkg.com/commander/-/commander-2.19.0.tgz#f6198aa84e5b83c46054b94ddedbfed5ee9ff12a" integrity sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg== -commander@~2.13.0: - version "2.13.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.13.0.tgz#6964bca67685df7c1f1430c584f07d7597885b9c" - integrity sha512-MVuS359B+YzaWqjCL/c+22gfryv+mCBPHAv3zyVI2GN8EY6IRP8VwtasXn8jyyhvvq84R4ImN1OKRtcbIasjYA== - commander@~2.17.1: version "2.17.1" resolved "https://registry.yarnpkg.com/commander/-/commander-2.17.1.tgz#bd77ab7de6de94205ceacc72f1716d29f20a77bf" @@ -2588,15 +2586,15 @@ crypto-random-string@^1.0.0: integrity sha1-ojD2T1aDEOFJgAmUB5DsmVRbyn4= css-loader@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-1.0.0.tgz#9f46aaa5ca41dbe31860e3b62b8e23c42916bf56" - integrity sha512-tMXlTYf3mIMt3b0dDCOQFJiVvxbocJ5Ho577WiGPYPZcqVEO218L2iU22pDXzkTZCLDE+9AmGSUkWxeh/nZReA== + version "1.0.1" + resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-1.0.1.tgz#6885bb5233b35ec47b006057da01cc640b6b79fe" + integrity sha512-+ZHAZm/yqvJ2kDtPne3uX0C+Vr3Zn5jFn2N4HywtS5ujwvsVkyg0VArEXpl3BgczDA8anieki1FIzhchX4yrDw== dependencies: babel-code-frame "^6.26.0" css-selector-tokenizer "^0.7.0" icss-utils "^2.1.0" loader-utils "^1.0.2" - lodash.camelcase "^4.3.0" + lodash "^4.17.11" postcss "^6.0.23" postcss-modules-extract-imports "^1.2.0" postcss-modules-local-by-default "^1.2.0" @@ -2977,7 +2975,7 @@ debug@~3.1.0: dependencies: ms "2.0.0" -decamelize@^1.1.1: +decamelize@^1.1.1, decamelize@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA= @@ -2989,10 +2987,10 @@ decamelize@^2.0.0: dependencies: xregexp "4.0.0" -deckar01-task_list@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/deckar01-task_list/-/deckar01-task_list-2.0.0.tgz#7f7a595430d21b3036ed5dfbf97d6b65de18e2c9" - integrity sha1-f3pZVDDSGzA27V37+X1rZd4Y4sk= +deckar01-task_list@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/deckar01-task_list/-/deckar01-task_list-2.0.1.tgz#fdcfb6ab5717055a82f29e863a49990a043a06a9" + integrity sha512-i5fT8QxJ9iV6dfgy5U0NHW91O5cKsvDc4u8JNMnZ6efQc356bA9vKuXO3732agSry+bO6TolzTmuqSRi4tkkeA== decode-uri-component@^0.2.0: version "0.2.0" @@ -3135,6 +3133,11 @@ destroy@~1.0.4: resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA= +detect-file@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/detect-file/-/detect-file-1.0.0.tgz#f0d66d03672a825cb1b73bdb3fe62310c8e552b7" + integrity sha1-8NZtA2cqglyxtzvbP+YjEMjlUrc= + detect-indent@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-4.0.0.tgz#f76d064352cdf43a1cb6ce619c4ee3a9475de208" @@ -3152,10 +3155,10 @@ detect-newline@^2.1.0: resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-2.1.0.tgz#f41f1c10be4b00e87b5f13da680759f2c5bfd3e2" integrity sha1-9B8cEL5LAOh7XxPaaAdZ8sW/0+I= -detect-node@^2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.0.3.tgz#a2033c09cc8e158d37748fbde7507832bd6ce127" - integrity sha1-ogM8CcyOFY03dI+951B4Mr1s4Sc= +detect-node@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.0.4.tgz#014ee8f8f669c5c58023da64b8179c083a28c46c" + integrity sha512-ZIzRpLJrOj7jjP2miAtgqIfmzbxa4ZOr5jJc601zklsfEx9oTzmmj2nVpIPRpNlRTIh8lc1kyViIY7BWSGNmKw== di@^0.0.1: version "0.0.1" @@ -3196,6 +3199,11 @@ dns-txt@^2.0.2: dependencies: buffer-indexof "^1.0.0" +docdash@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/docdash/-/docdash-1.0.2.tgz#0449a8f6bb247f563020b78a5485dea95ae2e094" + integrity sha512-IEM57bWPLtVXpUeCKbiGvHsHtW9O9ZiiBPfeQDAZ7JdQiAF3aNWQoJ3e/+uJ63lHO009yn0tbJjtKpXJ2AURCQ== + doctrine@1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-1.5.0.tgz#379dce730f6166f76cefa4e6707a159b02c5a6fa" @@ -3345,10 +3353,10 @@ ejs@^2.6.1: resolved "https://registry.yarnpkg.com/ejs/-/ejs-2.6.1.tgz#498ec0d495655abc6f23cd61868d926464071aa0" integrity sha512-0xy4A/twfrRCnkhfk8ErDi5DqdAsAqeGxht4xkCUrsvhhbQNs7E+4jV0CN7+NKIY0aHE72+XvqtBIXzD31ZbXQ== -electron-to-chromium@^1.3.62: - version "1.3.73" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.73.tgz#aa67787067d58cc3920089368b3b8d6fe0fc12f6" - integrity sha512-6PIg7v9zRoVGh6EheRF8h6Plti+3Yo/qtHobS4/Htyt53DNHmKKGFqSae1AIk0k1S4gCQvt7I2WgpbuZNcDY+g== +electron-to-chromium@^1.3.96: + version "1.3.100" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.100.tgz#899fb088def210aee6b838a47655bbb299190e13" + integrity sha512-cEUzis2g/RatrVf8x26L8lK5VEls1AGnLHk6msluBUg/NTB4wcXzExTsGscFq+Vs4WBBU2zbLLySvD4C0C3hwg== elliptic@^6.0.0: version "6.4.0" @@ -3432,7 +3440,7 @@ engine.io@~3.2.0: engine.io-parser "~2.1.0" ws "~3.3.1" -enhanced-resolve@^4.0.0, enhanced-resolve@^4.1.0: +enhanced-resolve@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-4.1.0.tgz#41c7e0bfdfe74ac1ffe1e57ad6a5c6c9f3742a7f" integrity sha512-F/7vkyTtyc/llOIn8oWclcB25KdRaiPBpZYDgJHgh/UHtpgT2p2eldQgtQnLtUvfMKPKxbRaQM/hHkvLHt1Vng== @@ -3504,7 +3512,7 @@ escape-html@~1.0.3: resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg= -escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5: +escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5, escape-string-regexp@~1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= @@ -3908,6 +3916,13 @@ expand-range@^1.8.1: dependencies: fill-range "^2.1.0" +expand-tilde@^2.0.0, expand-tilde@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/expand-tilde/-/expand-tilde-2.0.2.tgz#97e801aa052df02454de46b02bf621642cdc8502" + integrity sha1-l+gBqgUt8CRU3kawK/YhZCzchQI= + dependencies: + homedir-polyfill "^1.0.1" + expect@^23.6.0: version "23.6.0" resolved "https://registry.yarnpkg.com/expect/-/expect-23.6.0.tgz#1e0c8d3ba9a581c87bd71fb9bc8862d443425f98" @@ -4076,7 +4091,7 @@ fb-watchman@^2.0.0: dependencies: bser "^2.0.0" -figgy-pudding@^3.1.0, figgy-pudding@^3.5.1: +figgy-pudding@^3.5.1: version "3.5.1" resolved "https://registry.yarnpkg.com/figgy-pudding/-/figgy-pudding-3.5.1.tgz#862470112901c727a0e495a80744bd5baa1d6790" integrity sha512-vNKxJHTEKNThjfrdJwHc7brvM6eVevuO5nTj6ez8ZQ1qbXTvGthucRF7S4vf2cr71QVnT70V34v0S1DyQsti0w== @@ -4096,10 +4111,10 @@ file-entry-cache@^2.0.0: flat-cache "^1.2.1" object-assign "^4.0.1" -file-loader@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/file-loader/-/file-loader-2.0.0.tgz#39749c82f020b9e85901dcff98e8004e6401cfde" - integrity sha512-YCsBfd1ZGCyonOKLxPiKPdu+8ld9HAaMEvJewzz+b2eTF7uL5Zm/HdBF6FjCrpCMRq25Mi0U1gl4pwn2TlH7hQ== +file-loader@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/file-loader/-/file-loader-3.0.1.tgz#f8e0ba0b599918b51adfe45d66d1e771ad560faa" + integrity sha512-4sNIOXgtH/9WZq4NvlfU3Opn5ynUsqBwSLyM+I7UOwdGigTBYfVVQEwe/msZNX/j4pCJTIM14Fsw66Svo1oVrw== dependencies: loader-utils "^1.0.2" schema-utils "^1.0.0" @@ -4177,15 +4192,6 @@ find-babel-config@^1.1.0: json5 "^0.5.1" path-exists "^3.0.0" -find-cache-dir@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-1.0.0.tgz#9288e3e9e3cc3748717d39eade17cf71fc30ee6f" - integrity sha1-kojj6ePMN0hxfTnq3hfPcfww7m8= - dependencies: - commondir "^1.0.1" - make-dir "^1.0.0" - pkg-dir "^2.0.0" - find-cache-dir@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-2.0.0.tgz#4c1faed59f45184530fb9d7fa123a4d04a98472d" @@ -4222,6 +4228,16 @@ find-up@^3.0.0: dependencies: locate-path "^3.0.0" +findup-sync@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/findup-sync/-/findup-sync-2.0.0.tgz#9326b1488c22d1a6088650a86901b2d9a90a2cbc" + integrity sha1-kyaxSIwi0aYIhlCoaQGy2akKLLw= + dependencies: + detect-file "^1.0.0" + is-glob "^3.1.0" + micromatch "^3.0.4" + resolve-dir "^1.0.1" + flat-cache@^1.2.1: version "1.2.2" resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-1.2.2.tgz#fa86714e72c21db88601761ecf2f555d1abc6b96" @@ -4476,10 +4492,30 @@ global-dirs@^0.1.0: dependencies: ini "^1.3.4" -global-modules-path@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/global-modules-path/-/global-modules-path-2.1.0.tgz#923ec524e8726bb0c1a4ed4b8e21e1ff80c88bbb" - integrity sha512-3DrmGj2TP+96cABk9TfMp6f3knH/Y46dqvWznTU3Tf6/bDGLDAn15tFluQ7BcloykOcdY16U0WGq0BQblYOxJQ== +global-modules-path@^2.3.0: + version "2.3.1" + resolved "https://registry.yarnpkg.com/global-modules-path/-/global-modules-path-2.3.1.tgz#e541f4c800a1a8514a990477b267ac67525b9931" + integrity sha512-y+shkf4InI7mPRHSo2b/k6ix6+NLDtyccYv86whhxrSGX9wjPX1VMITmrDbE1eh7zkzhiWtW2sHklJYoQ62Cxg== + +global-modules@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-1.0.0.tgz#6d770f0eb523ac78164d72b5e71a8877265cc3ea" + integrity sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg== + dependencies: + global-prefix "^1.0.1" + is-windows "^1.0.1" + resolve-dir "^1.0.0" + +global-prefix@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/global-prefix/-/global-prefix-1.0.2.tgz#dbf743c6c14992593c655568cb66ed32c0122ebe" + integrity sha1-2/dDxsFJklk8ZVVoy2btMsASLr4= + dependencies: + expand-tilde "^2.0.2" + homedir-polyfill "^1.0.1" + ini "^1.3.4" + is-windows "^1.0.1" + which "^1.2.14" globals@^11.1.0, globals@^11.7.0: version "11.7.0" @@ -4561,10 +4597,10 @@ got@^8.0.3: url-parse-lax "^3.0.0" url-to-options "^1.0.1" -graceful-fs@^4.1.11, graceful-fs@^4.1.2: - version "4.1.11" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658" - integrity sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg= +graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.9: + version "4.1.15" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.15.tgz#ffb703e1066e8a0eeaa4c8b80ba9253eeefbfb00" + integrity sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA== graphlibrary@^2.2.0: version "2.2.0" @@ -4605,10 +4641,10 @@ gzip-size@^5.0.0: duplexer "^0.1.1" pify "^3.0.0" -handle-thing@^1.2.5: - version "1.2.5" - resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-1.2.5.tgz#fd7aad726bf1a5fd16dfc29b2f7a6601d27139c4" - integrity sha1-/Xqtcmvxpf0W38KbL3pmAdJxOcQ= +handle-thing@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-2.0.0.tgz#0e039695ff50c93fc288557d696f3c1dc6776754" + integrity sha512-d4sze1JNC454Wdo2fkuyzCr6aHcbL6PGGuFAz0Li/NcOm1tCHGnWDRmJP85dh9IhQErTc2svWFEX5xHIOo//kQ== handlebars@^4.0.1, handlebars@^4.0.11, handlebars@^4.0.3: version "4.0.12" @@ -4778,6 +4814,13 @@ home-or-tmp@^2.0.0: os-homedir "^1.0.0" os-tmpdir "^1.0.1" +homedir-polyfill@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/homedir-polyfill/-/homedir-polyfill-1.0.1.tgz#4c2bbc8a758998feebf5ed68580f76d46768b4bc" + integrity sha1-TCu8inWJmP7r9e1oWA921GdotLw= + dependencies: + parse-passwd "^1.0.0" + hoopy@^0.1.2: version "0.1.4" resolved "https://registry.yarnpkg.com/hoopy/-/hoopy-0.1.4.tgz#609207d661100033a9a9402ad3dea677381c1b1d" @@ -5021,7 +5064,7 @@ inquirer@3.0.6: strip-ansi "^3.0.0" through "^2.3.6" -inquirer@^6.0.0, inquirer@^6.1.0: +inquirer@^6.1.0: version "6.2.0" resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-6.2.0.tgz#51adcd776f661369dc1e894859c2560a224abdd8" integrity sha512-QIEQG4YyQ2UYZGDC4srMZ7BjHOmNk1lR2JQj5UknBapklm6WHA+VVH7N+sUdX3A7NeCfGF8o4X1S3Ao7nAcIeg== @@ -5408,7 +5451,7 @@ is-utf8@^0.2.0: resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72" integrity sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI= -is-windows@^1.0.2: +is-windows@^1.0.1, is-windows@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA== @@ -6043,11 +6086,41 @@ js-yaml@3.x, js-yaml@^3.12.0, js-yaml@^3.7.0: argparse "^1.0.7" esprima "^4.0.0" +js2xmlparser@~3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/js2xmlparser/-/js2xmlparser-3.0.0.tgz#3fb60eaa089c5440f9319f51760ccd07e2499733" + integrity sha1-P7YOqgicVED5MZ9RdgzNB+JJlzM= + dependencies: + xmlcreate "^1.0.1" + jsbn@~0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM= +jsdoc-vue@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/jsdoc-vue/-/jsdoc-vue-1.0.0.tgz#ff3ac1ba6bc4a74079bb79058a7bf0066e346235" + integrity sha1-/zrBumvEp0B5u3kFinvwBm40YjU= + +jsdoc@^3.5.5: + version "3.5.5" + resolved "https://registry.yarnpkg.com/jsdoc/-/jsdoc-3.5.5.tgz#484521b126e81904d632ff83ec9aaa096708fa4d" + integrity sha512-6PxB65TAU4WO0Wzyr/4/YhlGovXl0EVYfpKbpSroSj0qBxT4/xod/l40Opkm38dRHRdQgdeY836M0uVnJQG7kg== + dependencies: + babylon "7.0.0-beta.19" + bluebird "~3.5.0" + catharsis "~0.8.9" + escape-string-regexp "~1.0.5" + js2xmlparser "~3.0.0" + klaw "~2.0.0" + marked "~0.3.6" + mkdirp "~0.5.1" + requizzle "~0.2.1" + strip-json-comments "~2.0.1" + taffydb "2.6.2" + underscore "~1.8.3" + jsdom@^11.5.1: version "11.12.0" resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-11.12.0.tgz#1a80d40ddd378a1de59656e9e6dc5a3ba8657bc8" @@ -6135,6 +6208,13 @@ json5@^0.5.0, json5@^0.5.1: resolved "https://registry.yarnpkg.com/json5/-/json5-0.5.1.tgz#1eade7acc012034ad84e2396767ead9fa5495821" integrity sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE= +json5@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/json5/-/json5-2.1.0.tgz#e7a0c62c48285c628d20a10b85c89bb807c32850" + integrity sha512-8Mh9h6xViijj36g7Dxi+Y4S6hNGV96vcJZr/SrlHh1LR/pEn/8j/+qIBbs44YKl69Lrfctp4QD+AdWLTMqEZAQ== + dependencies: + minimist "^1.2.0" + jsprim@^1.2.2: version "1.4.1" resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2" @@ -6293,6 +6373,13 @@ kind-of@^6.0.0, kind-of@^6.0.2: resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.2.tgz#01146b36a6218e64e58f3a8d66de5d7fc6f6d051" integrity sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA== +klaw@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/klaw/-/klaw-2.0.0.tgz#59c128e0dc5ce410201151194eeb9cbf858650f6" + integrity sha1-WcEo4Nxc5BAgEVEZTuucv4WGUPY= + dependencies: + graceful-fs "^4.1.9" + kleur@^2.0.1: version "2.0.2" resolved "https://registry.yarnpkg.com/kleur/-/kleur-2.0.2.tgz#b704f4944d95e255d038f0cb05fb8a602c55a300" @@ -6351,6 +6438,11 @@ lie@~3.1.0: dependencies: immediate "~3.0.5" +lightercollective@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/lightercollective/-/lightercollective-0.1.0.tgz#70df102c530dcb8d0ccabfe6175a8d00d5f61300" + integrity sha512-J9tg5uraYoQKaWbmrzDDexbG6hHnMcWS1qLYgJSWE+mpA3U5OCSeMUhb+K55otgZJ34oFdR0ECvdIb3xuO5JOQ== + load-json-file@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0" @@ -6412,7 +6504,7 @@ locate-path@^3.0.0: p-locate "^3.0.0" path-exists "^3.0.0" -lodash.camelcase@4.3.0, lodash.camelcase@^4.3.0: +lodash.camelcase@4.3.0: version "4.3.0" resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6" integrity sha1-soqmKIorn8ZRA1x3EfZathkDMaY= @@ -6517,7 +6609,7 @@ lru-cache@2.2.x: resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-2.2.4.tgz#6c658619becf14031d0d0b594b16042ce4dc063d" integrity sha1-bGWGGb7PFAMdDQtZSxYELOTcBj0= -lru-cache@^4.0.1, lru-cache@^4.1.1, lru-cache@^4.1.2, lru-cache@^4.1.3: +lru-cache@^4.0.1, lru-cache@^4.1.2, lru-cache@^4.1.3: version "4.1.3" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.3.tgz#a1175cf3496dfc8436c156c334b4955992bce69c" integrity sha512-fFEhvcgzuIoJVUF8fYr5KR0YqxD238zgObTps31YdADwPPAp82a4M8TrckkWyx7ekNlf9aBcVn81cFwwXngrJA== @@ -6525,6 +6617,13 @@ lru-cache@^4.0.1, lru-cache@^4.1.1, lru-cache@^4.1.2, lru-cache@^4.1.3: pseudomap "^1.0.2" yallist "^2.1.2" +lru-cache@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" + integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w== + dependencies: + yallist "^3.0.2" + lz-string@^1.4.4: version "1.4.4" resolved "https://registry.yarnpkg.com/lz-string/-/lz-string-1.4.4.tgz#c0d8eaf36059f705796e1e344811cf4c498d3a26" @@ -6544,11 +6643,6 @@ makeerror@1.0.x: dependencies: tmpl "1.0.x" -mamacro@^0.0.3: - version "0.0.3" - resolved "https://registry.yarnpkg.com/mamacro/-/mamacro-0.0.3.tgz#ad2c9576197c9f1abf308d0787865bd975a3f3e4" - integrity sha512-qMEwh+UujcQ+kbz3T6V+wAmO2U8veoq2w+3wY8MquqwVA3jChfwY+Tk52GZKDfACEPjuZ7r2oJLejwpt8jtwTA== - map-age-cleaner@^0.1.1: version "0.1.2" resolved "https://registry.yarnpkg.com/map-age-cleaner/-/map-age-cleaner-0.1.2.tgz#098fb15538fd3dbe461f12745b0ca8568d4e3f74" @@ -6573,10 +6667,10 @@ map-visit@^1.0.0: dependencies: object-visit "^1.0.0" -marked@^0.3.12: - version "0.3.12" - resolved "https://registry.yarnpkg.com/marked/-/marked-0.3.12.tgz#7cf25ff2252632f3fe2406bde258e94eee927519" - integrity sha512-k4NaW+vS7ytQn6MgJn3fYpQt20/mOgYM5Ft9BYMfQJDz2QT6yEeS9XJ8k2Nw8JTeWK/znPPW2n3UJGzyYEiMoA== +marked@^0.3.12, marked@~0.3.6: + version "0.3.19" + resolved "https://registry.yarnpkg.com/marked/-/marked-0.3.19.tgz#5d47f709c4c9fc3c216b6d46127280f40b39d790" + integrity sha512-ea2eGWOqNxPcXv8dyERdSr/6FmzvWwzjMxpfGB/sbMccXoct+xY+YukPD+QTUZwyvK7BZwcr4m21WBOW41pAkg== match-at@^0.1.1: version "0.1.1" @@ -6692,7 +6786,7 @@ micromatch@^2.3.11: parse-glob "^3.0.4" regex-cache "^0.4.2" -micromatch@^3.1.4, micromatch@^3.1.6, micromatch@^3.1.8, micromatch@^3.1.9: +micromatch@^3.0.4, micromatch@^3.1.4, micromatch@^3.1.6, micromatch@^3.1.8, micromatch@^3.1.9: version "3.1.10" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23" integrity sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg== @@ -6793,22 +6887,6 @@ minizlib@^1.1.0: dependencies: minipass "^2.2.1" -mississippi@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/mississippi/-/mississippi-2.0.0.tgz#3442a508fafc28500486feea99409676e4ee5a6f" - integrity sha512-zHo8v+otD1J10j/tC+VNoGK9keCuByhKovAvdn74dmxJl9+mWHnx6EMsDN4lgRoMI/eYo2nchAxniIbUPb5onw== - dependencies: - concat-stream "^1.5.0" - duplexify "^3.4.2" - end-of-stream "^1.1.0" - flush-write-stream "^1.0.0" - from2 "^2.1.0" - parallel-transform "^1.1.0" - pump "^2.0.1" - pumpify "^1.3.3" - stream-each "^1.1.0" - through2 "^2.0.0" - mississippi@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/mississippi/-/mississippi-3.0.0.tgz#ea0a3291f97e0b5e8776b363d5f0a12d94c67022" @@ -6833,7 +6911,7 @@ mixin-deep@^1.2.0: for-in "^1.0.2" is-extendable "^1.0.1" -mkdirp@0.5.x, mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.0: +mkdirp@0.5.x, mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.0, mkdirp@~0.5.1: version "0.5.1" resolved "http://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" integrity sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM= @@ -6942,10 +7020,10 @@ negotiator@0.6.1: resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.1.tgz#2b327184e8992101177b28563fb5e7102acd0ca9" integrity sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk= -neo-async@^2.5.0: - version "2.5.0" - resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.5.0.tgz#76b1c823130cca26acfbaccc8fbaf0a2fa33b18f" - integrity sha512-nJmSswG4As/MkRq7QZFuH/sf/yuv8ODdMZrY4Bedjp77a5MK4A6s7YbBB64c9u79EBUOfXUXBvArmvzTD0X+6g== +neo-async@^2.5.0, neo-async@^2.6.0: + version "2.6.0" + resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.0.tgz#b9d15e4d71c6762908654b5183ed38b753340835" + integrity sha512-MFh0d/Wa7vkKO3Y3LlacqAEeHK0mckVqzDieUKTT+KGxi+zIpeVsFxymkIiRpbpDziHc290Xr9A1O4Om7otoRA== nice-try@^1.0.4: version "1.0.4" @@ -7033,10 +7111,10 @@ node-pre-gyp@^0.10.0: semver "^5.3.0" tar "^4" -node-releases@^1.0.0-alpha.11: - version "1.0.0-alpha.12" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.0.0-alpha.12.tgz#32e461b879ea76ac674e511d9832cf29da345268" - integrity sha512-VPB4rTPqpVyWKBHbSa4YPFme3+8WHsOSpvbp0Mfj0bWsC8TEjt4HQrLl1hsBDELlp1nB4lflSgSuGTYiuyaP7Q== +node-releases@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.3.tgz#aad9ce0dcb98129c753f772c0aa01360fb90fbd2" + integrity sha512-6VrvH7z6jqqNFY200kdB6HdzkgM96Oaj9v3dqGfgp6mF+cHmU4wyQKZ2/WPDRVoR0Jz9KqbamaBN0ZhdUaysUQ== dependencies: semver "^5.3.0" @@ -7095,6 +7173,11 @@ normalize-path@^2.0.1, normalize-path@^2.1.1: dependencies: remove-trailing-separator "^1.0.1" +normalize-path@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" + integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== + normalize-url@2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-2.0.1.tgz#835a9da1551fa26f70e92329069a23aa6574d7e6" @@ -7228,10 +7311,10 @@ object.pick@^1.3.0: dependencies: isobject "^3.0.1" -obuf@^1.0.0, obuf@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/obuf/-/obuf-1.1.1.tgz#104124b6c602c6796881a042541d36db43a5264e" - integrity sha1-EEEktsYCxnlogaBCVB0220OlJk4= +obuf@^1.0.0, obuf@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/obuf/-/obuf-1.1.2.tgz#09bea3343d41859ebd446292d11c9d4db619084e" + integrity sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg== on-finished@~2.3.0: version "2.3.0" @@ -7496,6 +7579,11 @@ parse-json@^4.0.0: error-ex "^1.3.1" json-parse-better-errors "^1.0.1" +parse-passwd@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/parse-passwd/-/parse-passwd-1.0.0.tgz#6d5b934a456993b23d37f40a382d6f1666a8e5c6" + integrity sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY= + parse5@4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/parse5/-/parse5-4.0.0.tgz#6d78656e3da8d78b4ec0b906f7c08ef1dfe3f608" @@ -7708,9 +7796,9 @@ posix-character-classes@^0.1.0: integrity sha1-AerA/jta9xoqbAL+q7jB/vfgDqs= postcss-modules-extract-imports@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-1.2.0.tgz#66140ecece38ef06bf0d3e355d69bf59d141ea85" - integrity sha1-ZhQOzs447wa/DT41XWm/WdFB6oU= + version "1.2.1" + resolved "https://registry.yarnpkg.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-1.2.1.tgz#dc87e34148ec7eab5f791f7cd5849833375b741a" + integrity sha512-6jt9XZwUhwmRUhb/CkyJY020PYaPJsCyt3UjbaWo6XEbH/94Hmv6MP7fG2C5NDU/BcHzyGYxNtHvM+LTf9HrYw== dependencies: postcss "^6.0.1" @@ -7786,10 +7874,10 @@ prettier@1.13.7: resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.13.7.tgz#850f3b8af784a49a6ea2d2eaa7ed1428a34b7281" integrity sha512-KIU72UmYPGk4MujZGYMFwinB7lOf2LsDNGSOC8ufevsrPLISrZbNJlWstRi3m0AMuszbH+EFSQ/r6w56RSPK6w== -prettier@1.15.2: - version "1.15.2" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.15.2.tgz#d31abe22afa4351efa14c7f8b94b58bb7452205e" - integrity sha512-YgPLFFA0CdKL4Eg2IHtUSjzj/BWgszDHiNQAe0VAIBse34148whfdzLagRL+QiKS+YfK5ftB6X4v/MBw8yCoug== +prettier@1.15.3: + version "1.15.3" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.15.3.tgz#1feaac5bdd181237b54dbe65d874e02a1472786a" + integrity sha512-gAU9AGAPMaKb3NNSUUuhhFAS7SCO4ALTN4nRIn6PJ075Qd28Yn2Ig2ahEJWdJwJmlEBTUfC7mMUSFy8MwsOCfg== pretty-format@^23.6.0: version "23.6.0" @@ -7897,7 +7985,7 @@ public-encrypt@^4.0.0: parse-asn1 "^5.0.0" randombytes "^2.0.1" -pump@^2.0.0, pump@^2.0.1: +pump@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/pump/-/pump-2.0.1.tgz#12399add6e4cf7526d973cbc8b5ce2e2908b3909" integrity sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA== @@ -8027,10 +8115,13 @@ raw-body@2.3.2: iconv-lite "0.4.19" unpipe "1.0.0" -raw-loader@^0.5.1: - version "0.5.1" - resolved "https://registry.yarnpkg.com/raw-loader/-/raw-loader-0.5.1.tgz#0c3d0beaed8a01c966d9787bf778281252a979aa" - integrity sha1-DD0L6u2KAclm2Xh793goElKpeao= +raw-loader@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/raw-loader/-/raw-loader-1.0.0.tgz#3f9889e73dadbda9a424bce79809b4133ad46405" + integrity sha512-Uqy5AqELpytJTRxYT4fhltcKPj0TyaEpzJDcGz7DFJi+pQOOi3GjR/DOdxTkTsF+NzhnldIoG6TORaBlInUuqA== + dependencies: + loader-utils "^1.1.0" + schema-utils "^1.0.0" rc@^1.0.1, rc@^1.1.6, rc@^1.2.7: version "1.2.8" @@ -8093,7 +8184,7 @@ read-pkg@^3.0.0: normalize-package-data "^2.3.2" path-type "^3.0.0" -"readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.4, readable-stream@^2.0.6, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.2.9, readable-stream@^2.3.0, readable-stream@^2.3.3, readable-stream@^2.3.6: +"readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.4, readable-stream@^2.0.6, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.0, readable-stream@^2.3.3, readable-stream@^2.3.6: version "2.3.6" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf" integrity sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw== @@ -8345,6 +8436,13 @@ requires-port@1.x.x, requires-port@^1.0.0: resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8= +requizzle@~0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/requizzle/-/requizzle-0.2.1.tgz#6943c3530c4d9a7e46f1cddd51c158fc670cdbde" + integrity sha1-aUPDUwxNmn5G8c3dUcFY/GcM294= + dependencies: + underscore "~1.6.0" + resolve-cwd@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-2.0.0.tgz#00a9f7387556e27038eae232caa372a6a59b665a" @@ -8352,6 +8450,14 @@ resolve-cwd@^2.0.0: dependencies: resolve-from "^3.0.0" +resolve-dir@^1.0.0, resolve-dir@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/resolve-dir/-/resolve-dir-1.0.1.tgz#79a40644c362be82f26effe739c9bb5382046f43" + integrity sha1-eaQGRMNivoLybv/nOcm7U4IEb0M= + dependencies: + expand-tilde "^2.0.0" + global-modules "^1.0.0" + resolve-from@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-1.0.1.tgz#26cbfe935d1aeeeabb29bc3fe5aeb01e93d44226" @@ -8511,7 +8617,7 @@ sax@^1.2.4: resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== -schema-utils@^0.4.0, schema-utils@^0.4.2, schema-utils@^0.4.4, schema-utils@^0.4.5: +schema-utils@^0.4.0, schema-utils@^0.4.4: version "0.4.5" resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-0.4.5.tgz#21836f0608aac17b78f9e3e24daff14a5ca13a3e" integrity sha512-yYrjb9TX2k/J1Y5UNy3KYdZq10xhYcF8nMpAW6o3hy6Q8WSIEf9lJHG/ePnOBfziPM3fvQwfOwa13U/Fh8qTfA== @@ -8877,7 +8983,7 @@ source-map-support@^0.4.15: dependencies: source-map "^0.5.6" -source-map-support@^0.5.6: +source-map-support@^0.5.6, source-map-support@~0.5.6: version "0.5.9" resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.9.tgz#41bc953b2534267ea2d605bccfa7bfa3111ced5f" integrity sha512-gR6Rw4MvUlYy83vP0vxoVNzM6t8MUXqNuRsuBmBHQDu1Fh6X015FrLdgoDKcNdkwGubozq0P4N0Q37UyFVr1EA== @@ -8929,30 +9035,28 @@ spdx-license-ids@^1.0.2: resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-1.2.2.tgz#c9df7a3424594ade6bd11900d596696dc06bac57" integrity sha1-yd96NCRZSt5r0RkA1ZZpbcBrrFc= -spdy-transport@^2.0.18: - version "2.0.20" - resolved "https://registry.yarnpkg.com/spdy-transport/-/spdy-transport-2.0.20.tgz#735e72054c486b2354fe89e702256004a39ace4d" - integrity sha1-c15yBUxIayNU/onnAiVgBKOazk0= +spdy-transport@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/spdy-transport/-/spdy-transport-3.0.0.tgz#00d4863a6400ad75df93361a1608605e5dcdcf31" + integrity sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw== dependencies: - debug "^2.6.8" - detect-node "^2.0.3" + debug "^4.1.0" + detect-node "^2.0.4" hpack.js "^2.1.6" - obuf "^1.1.1" - readable-stream "^2.2.9" - safe-buffer "^5.0.1" - wbuf "^1.7.2" + obuf "^1.1.2" + readable-stream "^3.0.6" + wbuf "^1.7.3" -spdy@^3.4.1: - version "3.4.7" - resolved "https://registry.yarnpkg.com/spdy/-/spdy-3.4.7.tgz#42ff41ece5cc0f99a3a6c28aabb73f5c3b03acbc" - integrity sha1-Qv9B7OXMD5mjpsKKq7c/XDsDrLw= +spdy@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/spdy/-/spdy-4.0.0.tgz#81f222b5a743a329aa12cea6a390e60e9b613c52" + integrity sha512-ot0oEGT/PGUpzf/6uk4AWLqkq+irlqHXkrdbk51oWONh3bxQmBuljxPNl66zlRRcIJStWq0QkLUCPOPjgjvU0Q== dependencies: - debug "^2.6.8" - handle-thing "^1.2.5" + debug "^4.1.0" + handle-thing "^2.0.0" http-deceiver "^1.2.7" - safe-buffer "^5.0.1" select-hose "^2.0.0" - spdy-transport "^2.0.18" + spdy-transport "^3.0.0" split-string@^3.0.1, split-string@^3.0.2: version "3.1.0" @@ -9001,14 +9105,7 @@ sshpk@^1.7.0: safer-buffer "^2.0.2" tweetnacl "~0.14.0" -ssri@^5.2.4: - version "5.2.4" - resolved "https://registry.yarnpkg.com/ssri/-/ssri-5.2.4.tgz#9985e14041e65fc397af96542be35724ac11da52" - integrity sha512-UnEAgMZa15973iH7cUi0AHjJn1ACDIkaMyZILoqwN6yzt+4P81I8tBc5Hl+qwi5auMplZtPQsHrPBR5vJLcQtQ== - dependencies: - safe-buffer "^5.1.1" - -ssri@^6.0.0: +ssri@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/ssri/-/ssri-6.0.1.tgz#2a3c41b28dd45b62b63676ecb74001265ae9edd8" integrity sha512-3Wge10hNcT1Kur4PDFwEieXSCMCJs/7WvSACcrMYrNp+b8kDL1/0wJch5Ni2WrtwEa2IO8OsVfeKIciKCDx/QA== @@ -9182,13 +9279,13 @@ strip-json-comments@^2.0.0, strip-json-comments@^2.0.1, strip-json-comments@~2.0 resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo= -style-loader@^0.23.0: - version "0.23.0" - resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-0.23.0.tgz#8377fefab68416a2e05f1cabd8c3a3acfcce74f1" - integrity sha512-uCcN7XWHkqwGVt7skpInW6IGO1tG6ReyFQ1Cseh0VcN6VdcFQi62aG/2F3Y9ueA8x4IVlfaSUxpmQXQD9QrEuQ== +style-loader@^0.23.1: + version "0.23.1" + resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-0.23.1.tgz#cb9154606f3e771ab6c4ab637026a1049174d925" + integrity sha512-XK+uv9kWwhZMZ1y7mysB+zoihsEj4wneFWAS5qoiLwzW0WzSqMrrsIy+a3zkQJq0ipFtBpX5W3MqyRIBF/WFGg== dependencies: loader-utils "^1.1.0" - schema-utils "^0.4.5" + schema-utils "^1.0.0" supports-color@^2.0.0: version "2.0.0" @@ -9202,7 +9299,7 @@ supports-color@^3.1.0, supports-color@^3.1.2: dependencies: has-flag "^1.0.0" -supports-color@^5.1.0, supports-color@^5.2.0, supports-color@^5.3.0, supports-color@^5.4.0: +supports-color@^5.1.0, supports-color@^5.2.0, supports-color@^5.3.0, supports-color@^5.4.0, supports-color@^5.5.0: version "5.5.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== @@ -9234,6 +9331,11 @@ table@^5.0.2: slice-ansi "2.0.0" string-width "^2.1.1" +taffydb@2.6.2: + version "2.6.2" + resolved "https://registry.yarnpkg.com/taffydb/-/taffydb-2.6.2.tgz#7cbcb64b5a141b6a2efc2c5d2c67b4e150b2a268" + integrity sha1-fLy2S1oUG2ou/CxdLGe04VCyomg= + tapable@^0.1.8: version "0.1.10" resolved "https://registry.yarnpkg.com/tapable/-/tapable-0.1.10.tgz#29c35707c2b70e50d07482b5d202e8ed446dafd4" @@ -9264,6 +9366,29 @@ term-size@^1.2.0: dependencies: execa "^0.7.0" +terser-webpack-plugin@^1.1.0: + version "1.2.1" + resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-1.2.1.tgz#7545da9ae5f4f9ae6a0ac961eb46f5e7c845cc26" + integrity sha512-GGSt+gbT0oKcMDmPx4SRSfJPE1XaN3kQRWG4ghxKQw9cn5G9x6aCKSsgYdvyM0na9NJ4Drv0RG6jbBByZ5CMjw== + dependencies: + cacache "^11.0.2" + find-cache-dir "^2.0.0" + schema-utils "^1.0.0" + serialize-javascript "^1.4.0" + source-map "^0.6.1" + terser "^3.8.1" + webpack-sources "^1.1.0" + worker-farm "^1.5.2" + +terser@^3.8.1: + version "3.14.1" + resolved "https://registry.yarnpkg.com/terser/-/terser-3.14.1.tgz#cc4764014af570bc79c79742358bd46926018a32" + integrity sha512-NSo3E99QDbYSMeJaEk9YW2lTg3qS9V0aKGlb+PlOrei1X02r1wSBHCNX/O+yeTRFSWPKPIGj6MqvvdqV4rnVGw== + dependencies: + commander "~2.17.1" + source-map "~0.6.1" + source-map-support "~0.5.6" + test-exclude@^4.2.1: version "4.2.3" resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-4.2.3.tgz#a9a5e64474e4398339245a0a769ad7c2f4a97c20" @@ -9513,14 +9638,6 @@ typescript@^2: resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.9.2.tgz#1cbf61d05d6b96269244eb6a3bce4bd914e0f00c" integrity sha512-Gr4p6nFNaoufRIY4NMdpQRNmgxVIGMs4Fcu/ujdYk3nAZqk7supzBE9idmvfZIlH/Cuj//dvi+019qEue9lV0w== -uglify-es@^3.3.4: - version "3.3.9" - resolved "https://registry.yarnpkg.com/uglify-es/-/uglify-es-3.3.9.tgz#0c1c4f0700bed8dbc124cdb304d2592ca203e677" - integrity sha512-r+MU0rfv4L/0eeW3xZrd16t4NZfK8Ld4SWVglYBb7ez5uXFWHuVRs6xCTrf1yirs9a4j4Y27nn7SRfO6v67XsQ== - dependencies: - commander "~2.13.0" - source-map "~0.6.1" - uglify-js@^3.1.4: version "3.4.9" resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.4.9.tgz#af02f180c1207d76432e473ed24a28f4a782bae3" @@ -9529,20 +9646,6 @@ uglify-js@^3.1.4: commander "~2.17.1" source-map "~0.6.1" -uglifyjs-webpack-plugin@^1.2.4: - version "1.2.5" - resolved "https://registry.yarnpkg.com/uglifyjs-webpack-plugin/-/uglifyjs-webpack-plugin-1.2.5.tgz#2ef8387c8f1a903ec5e44fa36f9f3cbdcea67641" - integrity sha512-hIQJ1yxAPhEA2yW/i7Fr+SXZVMp+VEI3d42RTHBgQd2yhp/1UdBcR3QEWPV5ahBxlqQDMEMTuTEvDHSFINfwSw== - dependencies: - cacache "^10.0.4" - find-cache-dir "^1.0.0" - schema-utils "^0.4.5" - serialize-javascript "^1.4.0" - source-map "^0.6.1" - uglify-es "^3.3.4" - webpack-sources "^1.1.0" - worker-farm "^1.5.2" - ultron@~1.1.0: version "1.1.1" resolved "https://registry.yarnpkg.com/ultron/-/ultron-1.1.1.tgz#9fe1536a10a664a65266a1e3ccf85fd36302bc9c" @@ -9555,11 +9658,28 @@ undefsafe@^2.0.2: dependencies: debug "^2.2.0" +underscore-contrib@~0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/underscore-contrib/-/underscore-contrib-0.3.0.tgz#665b66c24783f8fa2b18c9f8cbb0e2c7d48c26c7" + integrity sha1-ZltmwkeD+PorGMn4y7Dix9SMJsc= + dependencies: + underscore "1.6.0" + +underscore@1.6.0, underscore@~1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.6.0.tgz#8b38b10cacdef63337b8b24e4ff86d45aea529a8" + integrity sha1-izixDKze9jM3uLJOT/htRa6lKag= + underscore@^1.9.0: version "1.9.0" resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.9.0.tgz#31dbb314cfcc88f169cd3692d9149d81a00a73e4" integrity sha512-4IV1DSSxC1QK48j9ONFK1MoIAKKkbE8i7u55w2R6IqBqbT7A/iG7aZBCR2Bi8piF0Uz+i/MG1aeqLwl/5vqF+A== +underscore@~1.8.3: + version "1.8.3" + resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.8.3.tgz#4f3fb53b106e6097fcf9cb4109f2a5e9bdfa5022" + integrity sha1-Tz+1OxBuYJf8+ctBCfKl6b36UCI= + unicode-canonical-property-names-ecmascript@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz#2619800c4c825800efdd8343af7dd9933cbe2818" @@ -9598,10 +9718,10 @@ uniq@^1.0.1: resolved "https://registry.yarnpkg.com/uniq/-/uniq-1.0.1.tgz#b31c5ae8254844a3a8281541ce2b04b865a734ff" integrity sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8= -unique-filename@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/unique-filename/-/unique-filename-1.1.0.tgz#d05f2fe4032560871f30e93cbe735eea201514f3" - integrity sha1-0F8v5AMlYIcfMOk8vnNe6iAVFPM= +unique-filename@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/unique-filename/-/unique-filename-1.1.1.tgz#1d69769369ada0583103a1e6ae87681b56573230" + integrity sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ== dependencies: unique-slug "^2.0.0" @@ -9669,10 +9789,10 @@ urix@^0.1.0: resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72" integrity sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI= -url-loader@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/url-loader/-/url-loader-1.1.1.tgz#4d1f3b4f90dde89f02c008e662d604d7511167c1" - integrity sha512-vugEeXjyYFBCUOpX+ZuaunbK3QXMKaQ3zUnRfIpRBlGkY7QizCnzyyn2ASfcxsvyU3ef+CJppVywnl3Kgf13Gg== +url-loader@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/url-loader/-/url-loader-1.1.2.tgz#b971d191b83af693c5e3fea4064be9e1f2d7f8d8" + integrity sha512-dXHkKmw8FhPqu8asTc1puBfe3TehOCo2+RmOOev5suNCIYBcT626kxiWg1NBVkwc4rO8BGa7gP70W7VXuqHrjg== dependencies: loader-utils "^1.1.0" mime "^2.0.3" @@ -9765,10 +9885,10 @@ uuid@^3.0.1, uuid@^3.3.2: resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131" integrity sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA== -v8-compile-cache@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.0.0.tgz#526492e35fc616864284700b7043e01baee09f0a" - integrity sha512-qNdTUMaCjPs4eEnM3W9H94R3sU70YCuT+/ST7nUf+id1bVOrdjrpUaeZLqPBPRph3hsgn4a4BvwpxhHZx+oSDg== +v8-compile-cache@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.0.2.tgz#a428b28bb26790734c4fc8bc9fa106fccebf6a6c" + integrity sha512-1wFuMUIM16MDJRCrpbpuEPTUGmM5QMUg0cr3KFwra2XgOgFcPGDQHDh3CszSCD2Zewc/dh/pamNEW8CbfDebUw== validate-npm-package-license@^3.0.1: version "3.0.1" @@ -9839,10 +9959,10 @@ vue-hot-reload-api@^2.3.0: resolved "https://registry.yarnpkg.com/vue-hot-reload-api/-/vue-hot-reload-api-2.3.0.tgz#97976142405d13d8efae154749e88c4e358cf926" integrity sha512-2j/t+wIbyVMP5NvctQoSUvLkYKoWAAk2QlQiilrM2a6/ulzFgdcLUJfTvs4XQ/3eZhHiBmmEojbjmM4AzZj8JA== -vue-jest@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/vue-jest/-/vue-jest-3.0.1.tgz#127cded1a57cdfcf01fa8a10ce29579e2cb3a04d" - integrity sha512-otS+n341cTsp0pF7tuTu2x43b23x/+K0LZdAXV+ewKYIMZRqhuQaJTECWEt/cN/YZw2JC6hUM6xybdnOB4ZQ+g== +vue-jest@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/vue-jest/-/vue-jest-3.0.2.tgz#c64bf5da9abd0d3ee16071217037696d14b0689c" + integrity sha512-5XIQ1xQFW0ZnWxHWM7adVA2IqbDsdw1vhgZfGFX4oWd75J38KIS3YT41PtiE7lpMLmNM6+VJ0uprT2mhHjUgkA== dependencies: babel-plugin-transform-es2015-modules-commonjs "^6.26.0" chalk "^2.1.0" @@ -9866,17 +9986,17 @@ vue-loader@^15.4.2: vue-hot-reload-api "^2.3.0" vue-style-loader "^4.1.0" -vue-resource@^1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/vue-resource/-/vue-resource-1.5.0.tgz#ba0c6ef7af2eeace03cf24a91f529471be974c72" - integrity sha512-em+Ihe+duUWQv4uKO8aFTGK+e/lvNtk5EBEmWaBYcfQzBmHhKR4jJAeVIHcG6otugmsme/DmYrOEPfbss+2XfQ== +vue-resource@^1.5.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/vue-resource/-/vue-resource-1.5.1.tgz#0f3d685e3254d21800bebd966edcf56c34b3b6e4" + integrity sha512-o6V4wNgeqP+9v9b2bPXrr20CGNQPEXjpbUWdZWq9GJhqVeAGcYoeTtn/D4q059ZiyN0DIrDv/ADrQUmlUQcsmg== dependencies: got "^8.0.3" -vue-router@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-3.0.1.tgz#d9b05ad9c7420ba0f626d6500d693e60092cc1e9" - integrity sha512-vLLoY452L+JBpALMP5UHum9+7nzR9PeIBCghU9ZtJ1eWm6ieUI8Zb/DI3MYxH32bxkjzYV1LRjNv4qr8d+uX/w== +vue-router@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-3.0.2.tgz#dedc67afe6c4e2bc25682c8b1c2a8c0d7c7e56be" + integrity sha512-opKtsxjp9eOcFWdp6xLQPLmRGgfM932Tl56U9chYTnoWqKxQ8M20N7AkdEbM5beUh6wICoFGYugAX9vQjyJLFg== vue-style-loader@^4.1.0: version "4.1.0" @@ -9886,7 +10006,7 @@ vue-style-loader@^4.1.0: hash-sum "^1.0.2" loader-utils "^1.0.2" -vue-template-compiler@^2.5.0, vue-template-compiler@^2.5.17: +vue-template-compiler@^2.5.0, vue-template-compiler@^2.5.21: version "2.5.21" resolved "https://registry.yarnpkg.com/vue-template-compiler/-/vue-template-compiler-2.5.21.tgz#a57ceb903177e8f643560a8d639a0f8db647054a" integrity sha512-Vmk5Cv7UcmI99B9nXJEkaK262IQNnHp5rJYo+EwYpe2epTAXqcVyExhV6pk8jTkxQK2vRc8v8KmZBAwdmUZvvw== @@ -9904,7 +10024,7 @@ vue-virtual-scroll-list@^1.2.5: resolved "https://registry.yarnpkg.com/vue-virtual-scroll-list/-/vue-virtual-scroll-list-1.2.5.tgz#bcbd010f7cdb035eba8958ebf807c6214d9a167a" integrity sha1-vL0BD3zbA166iVjr+AfGIU2aFno= -vue@^2.5.17, vue@^2.5.21: +vue@^2.5.21: version "2.5.21" resolved "https://registry.yarnpkg.com/vue/-/vue-2.5.21.tgz#3d33dcd03bb813912ce894a8303ab553699c4a85" integrity sha512-Aejvyyfhn0zjVeLvXd70h4hrE4zZDx1wfZqia6ekkobLmUZ+vNFQer53B4fu0EjWBSiqApxPejzkO1Znt3joxQ== @@ -9945,10 +10065,10 @@ watchpack@^1.5.0: graceful-fs "^4.1.2" neo-async "^2.5.0" -wbuf@^1.1.0, wbuf@^1.7.2: - version "1.7.2" - resolved "https://registry.yarnpkg.com/wbuf/-/wbuf-1.7.2.tgz#d697b99f1f59512df2751be42769c1580b5801fe" - integrity sha1-1pe5nx9ZUS3ydRvkJ2nBWAtYAf4= +wbuf@^1.1.0, wbuf@^1.7.3: + version "1.7.3" + resolved "https://registry.yarnpkg.com/wbuf/-/wbuf-1.7.3.tgz#c1d8d149316d3ea852848895cb6a0bfe887b87df" + integrity sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA== dependencies: minimalistic-assert "^1.0.0" @@ -9957,10 +10077,10 @@ webidl-conversions@^4.0.2: resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad" integrity sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg== -webpack-bundle-analyzer@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/webpack-bundle-analyzer/-/webpack-bundle-analyzer-3.0.2.tgz#22f19ea6d1b5a15fd7a90baae0bc0f39bd1e4d48" - integrity sha512-cZG4wSQtKrSpk5RJ33dxiaAyo8bP0V+JvycAyIDFEiDIhw4LHhhVKhn40YT1w6TR9E4scHA00LnIoBtTA13Mow== +webpack-bundle-analyzer@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/webpack-bundle-analyzer/-/webpack-bundle-analyzer-3.0.3.tgz#dbc7fff8f52058b6714a20fddf309d0790e3e0a0" + integrity sha512-naLWiRfmtH4UJgtUktRTLw6FdoZJ2RvCR9ePbwM9aRMsS/KjFerkPZG9epEvXRAw5d5oPdrs9+3p+afNjxW8Xw== dependencies: acorn "^5.7.3" bfj "^6.1.1" @@ -9975,22 +10095,24 @@ webpack-bundle-analyzer@^3.0.2: opener "^1.5.1" ws "^6.0.0" -webpack-cli@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-3.1.0.tgz#d71a83687dcfeb758fdceeb0fe042f96bcf62994" - integrity sha512-p5NeKDtYwjZozUWq6kGNs9w+Gtw/CPvyuXjXn2HMdz8Tie+krjEg8oAtonvIyITZdvpF7XG9xDHwscLr2c+ugQ== +webpack-cli@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-3.2.1.tgz#779c696c82482491f0803907508db2e276ed3b61" + integrity sha512-jeJveHwz/vwpJ3B8bxEL5a/rVKIpRNJDsKggfKnxuYeohNDW4Y/wB9N/XHJA093qZyS0r6mYL+/crLsIol4WKA== dependencies: chalk "^2.4.1" cross-spawn "^6.0.5" - enhanced-resolve "^4.0.0" - global-modules-path "^2.1.0" - import-local "^1.0.0" - inquirer "^6.0.0" + enhanced-resolve "^4.1.0" + findup-sync "^2.0.0" + global-modules "^1.0.0" + global-modules-path "^2.3.0" + import-local "^2.0.0" interpret "^1.1.0" + lightercollective "^0.1.0" loader-utils "^1.1.0" - supports-color "^5.4.0" - v8-compile-cache "^2.0.0" - yargs "^12.0.1" + supports-color "^5.5.0" + v8-compile-cache "^2.0.2" + yargs "^12.0.4" webpack-dev-middleware@3.4.0, webpack-dev-middleware@^3.2.0: version "3.4.0" @@ -10002,10 +10124,10 @@ webpack-dev-middleware@3.4.0, webpack-dev-middleware@^3.2.0: range-parser "^1.0.3" webpack-log "^2.0.0" -webpack-dev-server@^3.1.10: - version "3.1.10" - resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-3.1.10.tgz#507411bee727ee8d2fdffdc621b66a64ab3dea2b" - integrity sha512-RqOAVjfqZJtQcB0LmrzJ5y4Jp78lv9CK0MZ1YJDTaTmedMZ9PU9FLMQNrMCfVu8hHzaVLVOJKBlGEHMN10z+ww== +webpack-dev-server@^3.1.14: + version "3.1.14" + resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-3.1.14.tgz#60fb229b997fc5a0a1fc6237421030180959d469" + integrity sha512-mGXDgz5SlTxcF3hUpfC8hrQ11yhAttuUQWf1Wmb+6zo3x6rb7b9mIfuQvAPLdfDRCGRGvakBWHdHOa0I9p/EVQ== dependencies: ansi-html "0.0.7" bonjour "^3.5.0" @@ -10026,12 +10148,14 @@ webpack-dev-server@^3.1.10: portfinder "^1.0.9" schema-utils "^1.0.0" selfsigned "^1.9.1" + semver "^5.6.0" serve-index "^1.7.2" sockjs "0.3.19" sockjs-client "1.3.0" - spdy "^3.4.1" + spdy "^4.0.0" strip-ansi "^3.0.0" supports-color "^5.1.0" + url "^0.11.0" webpack-dev-middleware "3.4.0" webpack-log "^2.0.0" yargs "12.0.2" @@ -10044,7 +10168,7 @@ webpack-log@^2.0.0: ansi-colors "^3.0.0" uuid "^3.3.2" -webpack-sources@^1.0.1, webpack-sources@^1.1.0, webpack-sources@^1.2.0: +webpack-sources@^1.0.1, webpack-sources@^1.1.0, webpack-sources@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.3.0.tgz#2a28dcb9f1f45fe960d8f1493252b5ee6530fa85" integrity sha512-OiVgSrbGu7NEnEvQJJgdSFPl2qWKkWq5lHMhgiToIiN9w34EBnjYzSYs+VbL5KoYiLNtFFa7BZIKxRED3I32pA== @@ -10057,15 +10181,15 @@ webpack-stats-plugin@^0.2.1: resolved "https://registry.yarnpkg.com/webpack-stats-plugin/-/webpack-stats-plugin-0.2.1.tgz#1f5bac13fc25d62cbb5fd0ff646757dc802b8595" integrity sha512-OYMZLpZrK/qLA79NE4kC4DCt85h/5ipvWJcsefKe9MMw0qU4/ck/IJg+4OmWA+5EfrZZpHXDq92IptfYDWVfkw== -webpack@^4.19.1: - version "4.19.1" - resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.19.1.tgz#096674bc3b573f8756c762754366e5b333d6576f" - integrity sha512-j7Q/5QqZRqIFXJvC0E59ipLV5Hf6lAnS3ezC3I4HMUybwEDikQBVad5d+IpPtmaQPQArvgUZLXIN6lWijHBn4g== +webpack@^4.28.1: + version "4.28.1" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.28.1.tgz#d0e2856e75d1224b170bf16c30b6ca9b75f0d958" + integrity sha512-qAS7BFyS5iuOZzGJxyDXmEI289h7tVNtJ5XMxf6Tz55J2riOyH42uaEsWF0F32TRaI+54SmI6qRgHM3GzsZ+sQ== dependencies: - "@webassemblyjs/ast" "1.7.6" - "@webassemblyjs/helper-module-context" "1.7.6" - "@webassemblyjs/wasm-edit" "1.7.6" - "@webassemblyjs/wasm-parser" "1.7.6" + "@webassemblyjs/ast" "1.7.11" + "@webassemblyjs/helper-module-context" "1.7.11" + "@webassemblyjs/wasm-edit" "1.7.11" + "@webassemblyjs/wasm-parser" "1.7.11" acorn "^5.6.2" acorn-dynamic-import "^3.0.0" ajv "^6.1.0" @@ -10083,9 +10207,9 @@ webpack@^4.19.1: node-libs-browser "^2.0.0" schema-utils "^0.4.4" tapable "^1.1.0" - uglifyjs-webpack-plugin "^1.2.4" + terser-webpack-plugin "^1.1.0" watchpack "^1.5.0" - webpack-sources "^1.2.0" + webpack-sources "^1.3.0" websocket-driver@>=0.5.1: version "0.6.5" @@ -10134,7 +10258,7 @@ which-module@^2.0.0: resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho= -which@^1.1.1, which@^1.2.1, which@^1.2.12, which@^1.2.9, which@^1.3.0: +which@^1.1.1, which@^1.2.1, which@^1.2.12, which@^1.2.14, which@^1.2.9, which@^1.3.0: version "1.3.1" resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== @@ -10253,6 +10377,11 @@ xmlbuilder@8.2.2: resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-8.2.2.tgz#69248673410b4ba42e1a6136551d2922335aa773" integrity sha1-aSSGc0ELS6QuGmE2VR0pIjNap3M= +xmlcreate@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/xmlcreate/-/xmlcreate-1.0.2.tgz#fa6bf762a60a413fb3dd8f4b03c5b269238d308f" + integrity sha1-+mv3YqYKQT+z3Y9LA8WyaSONMI8= + xmlhttprequest-ssl@~1.5.4: version "1.5.5" resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.5.tgz#c2876b06168aadc40e57d97e81191ac8f4398b3e" @@ -10305,6 +10434,14 @@ yargs-parser@^10.1.0: dependencies: camelcase "^4.1.0" +yargs-parser@^11.1.1: + version "11.1.1" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-11.1.1.tgz#879a0865973bca9f6bab5cbdf3b1c67ec7d3bcf4" + integrity sha512-C6kB/WJDiaxONLJQnF8ccx9SEeoTTLek8RVbaOIsrAUS8VrBEXfmeSnCZxygc+XC2sNMBIwOOnfcxiynjHsVSQ== + dependencies: + camelcase "^5.0.0" + decamelize "^1.2.0" + yargs-parser@^9.0.2: version "9.0.2" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-9.0.2.tgz#9ccf6a43460fe4ed40a9bb68f48d43b8a68cc077" @@ -10312,7 +10449,7 @@ yargs-parser@^9.0.2: dependencies: camelcase "^4.1.0" -yargs@12.0.2, yargs@^12.0.1: +yargs@12.0.2: version "12.0.2" resolved "https://registry.yarnpkg.com/yargs/-/yargs-12.0.2.tgz#fe58234369392af33ecbef53819171eff0f5aadc" integrity sha512-e7SkEx6N6SIZ5c5H22RTZae61qtn3PYUE8JYbBFlK9sYmh3DMQ6E5ygtaG/2BW0JZi4WGgTR2IV5ChqlqrDGVQ== @@ -10348,6 +10485,24 @@ yargs@^11.0.0: y18n "^3.2.1" yargs-parser "^9.0.2" +yargs@^12.0.4: + version "12.0.5" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-12.0.5.tgz#05f5997b609647b64f66b81e3b4b10a368e7ad13" + integrity sha512-Lhz8TLaYnxq/2ObqHDql8dX8CJi97oHxrjUcYtzKbbykPtVW9WB+poxI+NM2UIzsMgNCZTIf0AQwsjK5yMAqZw== + dependencies: + cliui "^4.0.0" + decamelize "^1.2.0" + find-up "^3.0.0" + get-caller-file "^1.0.1" + os-locale "^3.0.0" + require-directory "^2.1.1" + require-main-filename "^1.0.1" + set-blocking "^2.0.0" + string-width "^2.0.0" + which-module "^2.0.0" + y18n "^3.2.1 || ^4.0.0" + yargs-parser "^11.1.1" + yarn-deduplicate@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/yarn-deduplicate/-/yarn-deduplicate-1.0.5.tgz#e56016f1c29e77e323f401ea838f5e8c7cdbfd42" |