diff options
author | Dennis Tang <dennis@dennistang.net> | 2018-08-20 20:59:11 +0000 |
---|---|---|
committer | Dennis Tang <dennis@dennistang.net> | 2018-08-20 20:59:11 +0000 |
commit | fb28b3b375f54a7c14a395eee366cd2509a309be (patch) | |
tree | 859f7f3c28cf9012958ae61a834c02e8a0785cd6 | |
parent | e8c69b9581436e7bae9a5ff882948efdd66f7e3c (diff) | |
parent | 5874b2af0ba3710faf2d247e5a63e867e2b5de7a (diff) | |
download | gitlab-ce-44704-improve-project-overview-ui-buttons.tar.gz |
Merge branch '44704-improve-project-overview-ui' into '44704-improve-project-overview-ui-buttons'44704-improve-project-overview-ui-buttons
# Conflicts:
# app/assets/stylesheets/pages/projects.scss
708 files changed, 10987 insertions, 2926 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index fcf47421b01..797a20ef16e 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -453,6 +453,7 @@ danger-review: - master variables: - $CI_COMMIT_REF_NAME =~ /^ce-to-ee-.*/ + - $CI_COMMIT_REF_NAME =~ /.*-stable(-ee)?-prepare-.*/ script: - git version - danger --fail-on-errors=true @@ -738,7 +739,7 @@ karma: - chrome_debug.log - coverage-javascript/ -codequality: +code_quality: <<: *dedicated-no-docs-no-db-pull-cache-job image: docker:stable allow_failure: true @@ -756,9 +757,13 @@ codequality: script: # Extract "MAJOR.MINOR" from CI_SERVER_VERSION and generate "MAJOR-MINOR-stable" for Security Products - export SP_VERSION=$(echo "$CI_SERVER_VERSION" | sed 's/^\([0-9]*\)\.\([0-9]*\).*/\1-\2-stable/') - - docker run --env SOURCE_CODE="$PWD" --volume "$PWD":/code --volume /var/run/docker.sock:/var/run/docker.sock "registry.gitlab.com/gitlab-org/security-products/codequality:$SP_VERSION" /code + - docker run + --env SOURCE_CODE="$PWD" + --volume "$PWD":/code + --volume /var/run/docker.sock:/var/run/docker.sock + "registry.gitlab.com/gitlab-org/security-products/codequality:$SP_VERSION" /code artifacts: - paths: [codeclimate.json] + paths: [gl-code-quality-report.json] expire_in: 1 week sast: diff --git a/.gitlab/issue_templates/Acceptance_Testing.md b/.gitlab/issue_templates/Acceptance_Testing.md new file mode 100644 index 00000000000..f1fbb96ce61 --- /dev/null +++ b/.gitlab/issue_templates/Acceptance_Testing.md @@ -0,0 +1,100 @@ +## Details +- **Feature Toggle Name**: `FEATURE_NAME` +- **Required GitLab Version**: `vX.X` + +-------------------------------------------------------------------------------- + +## 1. Preparation + +- [ ] **Controllers and workers**: + 1. Please link to dashboards of the workers, and the controllers and actions that can be impacted + 2. ... + 3. ... + +## 2. Development Trial + +#### Check Dev Server Versions +- [ ] GitLab: https://dev.gitlab.org/help + +#### Enable on `dev.gitlab.org`: +- [ ] `/chatops feature set FEATURE_NAME true --dev` in [`#dev-gitlab`](https://gitlab.slack.com/messages/C6WQ87MU3) + +Then leave running while monitoring and performing some testing through web, api or SSH. + +#### Monitor + +- [ ] [Monitor Using Grafana](https://dashboards.gitlab.net) +- [ ] [Inspect logs in ELK](https://log.gitlab.net/app/kibana) +- [ ] [Check for errors in GitLab Dev Sentry](https://sentry.gitlap.com/gitlab/devgitlaborg/?query=is%3Aunresolved) + +## 2. Staging Trial + +#### Check Staging Server Versions +- [ ] GitLab: https://staging.gitlab.com/help + +#### Enable on `staging.gitlab.com` +- [ ] `/chatops run feature set FEATURE_NAME true --staging` in [`#development`](https://gitlab.slack.com/messages/C02PF508L/) + +Then leave running while monitoring for at least **15 minutes** while performing some testing through web, api or SSH. + +#### Monitor + +- [ ] [Monitor Using Grafana](https://dashboards.gitlab.net) +- [ ] [Inspect logs in ELK](https://log.gitlab.net/app/kibana) +- [ ] [Check for errors in GitLab Sentry](https://sentry.gitlap.com/gitlab/gitlabcom/?query=is%3Aunresolved) + +## 4. Production Server Version Check + +- [ ] GitLab: https://gitlab.com/help + +## 5. Initial Impact Check + +- [ ] Enable for a subset of users, when using percentage gates: 1%. + +Then leave running while monitoring for at least **15 minutes** while performing some testing through web, api or SSH. + +#### Monitor + +- [ ] [Monitor Using Grafana](https://dashboards.gitlab.net) +- [ ] [Inspect logs in ELK](https://log.gitlab.net/app/kibana) +- [ ] [Check for errors in GitLab Sentry](https://sentry.gitlap.com/gitlab/gitlabcom/?query=is%3Aunresolved) + +## 6. Low Impact Check + +- [ ] Enable for a bigger subset of users, when using percentage gates: 10%. + +Then leave running while monitoring for at least **30 minutes** while performing some testing through web, api or SSH. + +#### Monitor + +- [ ] [Monitor Using Grafana](https://dashboards.gitlab.net) +- [ ] [Inspect logs in ELK](https://log.gitlab.net/app/kibana) +- [ ] [Check for errors in GitLab Sentry](https://sentry.gitlap.com/gitlab/gitlabcom/?query=is%3Aunresolved) + +## 7. Mid Impact Trial + +- [ ] Enable for a big subset of users, when using percentage gates: 50%. + +Then leave running while monitoring for at least **12 hours** while performing some testing through web, api or SSH. + +#### Monitor + +- [ ] [Monitor Using Grafana](https://dashboards.gitlab.net) +- [ ] [Inspect logs in ELK](https://log.gitlab.net/app/kibana) +- [ ] [Check for errors in GitLab Sentry](https://sentry.gitlap.com/gitlab/gitlabcom/?query=is%3Aunresolved) + +## 8. Full Impact Trial + +- [ ] Enable for all users: `/chatops run feature set FEATURE_NAME true + +Then leave running while monitoring for at least **1 week**. + +#### Monitor + +- [ ] [Monitor Using Grafana](https://dashboards.gitlab.net) +- [ ] [Inspect logs in ELK](https://log.gitlab.net/app/kibana) +- [ ] [Check for errors in GitLab Dev Sentry](https://sentry.gitlap.com/gitlab/devgitlaborg/?query=is%3Aunresolved) + +#### Success? + +- [ ] Remove the feature gate from the code, and close this issue with that MR. diff --git a/.gitlab/issue_templates/Documentation.md b/.gitlab/issue_templates/Documentation.md new file mode 100644 index 00000000000..b33ed5bcaa8 --- /dev/null +++ b/.gitlab/issue_templates/Documentation.md @@ -0,0 +1,54 @@ +<!--See the general documentation guidelines https://docs.gitlab.com/ee/development/documentation --> + +<!-- Mention "documentation" or "docs" in the issue title --> + +<!-- Use this description template for new docs or updates to existing docs. --> + +<!-- Check the documentation structure guidelines for guidance: https://docs.gitlab.com/ee/development/documentation/structure.html--> + +- [ ] Documents Feature A <!-- feature name --> +- [ ] Follow-up from: #XXX, !YYY <!-- Mention related issues, MRs, and epics when available --> + +## New doc or update? + +<!-- Mark either of these boxes: --> + +- [ ] New documentation +- [ ] Update existing documentation + +## Checklists + +### Product Manager + +<!-- Reference: https://docs.gitlab.com/ee/development/documentation/workflow.html#1-product-manager-s-role-in-the-documentation-process --> + +- [ ] Add the correct labels +- [ ] Add the correct milestone +- [ ] Indicate the correct document/directory for this feature <!-- (ping the tech writers for help if you're not sure) --> +- [ ] Fill the doc blurb below + +#### Documentation blurb + +<!-- Documentation template: https://docs.gitlab.com/ee/development/documentation/structure.html#documentation-template-for-new-docs --> + +- Doc **title** + + <!-- write the doc title here --> + +- Feature **overview/description** + + <!-- Write the feature overview here --> + +- Feature **use cases** + + <!-- Write the use cases here --> + +### Developer + +<!-- Reference: https://docs.gitlab.com/ee/development/documentation/workflow.html#2-developer-s-role-in-the-documentation-process --> + +- [ ] Copy the doc blurb above and paste it into the doc +- [ ] Write the tutorial - explain how to use the feature +- [ ] Submit the MR using the appropriate MR description template + +/label ~Documentation diff --git a/.gitlab/issue_templates/Security developer workflow.md b/.gitlab/issue_templates/Security developer workflow.md index c1f702e9385..64b54b171f7 100644 --- a/.gitlab/issue_templates/Security developer workflow.md +++ b/.gitlab/issue_templates/Security developer workflow.md @@ -12,7 +12,7 @@ Set the title to: `[Security] Description of the original issue` - [ ] Link to the original issue adding it to the [links section](#links) - [ ] Run `scripts/security-harness` in the CE, EE, and/or Omnibus to prevent pushing to any remote besides `dev.gitlab.org` - [ ] Create an MR targetting `org` `master`, prefixing your branch with `security-` -- [ ] Label your MR with the ~security label, prefix the title with `WIP: [master]` +- [ ] Label your MR with the ~security label, prefix the title with `WIP: [master]` - [ ] Add a link to the MR to the [links section](#links) - [ ] Add a link to an EE MR if required - [ ] Make sure the MR remains in-progress and gets approved after the review cycle, **but never merged**. @@ -22,13 +22,13 @@ Set the title to: `[Security] Description of the original issue` - [ ] Once the MR is ready to be merged, create MRs targetting the last 3 releases - [ ] 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 [seckpick documentation] + - 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) - [ ] Create each MR targetting the security branch `security-X-Y` - [ ] Add the ~security label and prefix with the version `WIP: [X.Y]` the title of the MR - [ ] Make sure all MRs have a link in the [links section](#links) and are assigned to a Release Manager. -[seckpick documentation]: https://gitlab.com/gitlab-org/release/docs/blob/master/general/security/developer.md#secpick-script +[secpick documentation]: https://gitlab.com/gitlab-org/release/docs/blob/master/general/security/developer.md#secpick-script #### Documentation and final details @@ -68,4 +68,4 @@ Set the title to: `[Security] Description of the original issue` [security process for developers]: https://gitlab.com/gitlab-org/release/docs/blob/master/general/security/developer.md [RM list]: https://about.gitlab.com/release-managers/ -/label ~security +/label ~security diff --git a/.gitlab/merge_request_templates/Change documentation location.md b/.gitlab/merge_request_templates/Change documentation location.md new file mode 100644 index 00000000000..b4a6d2bd3b4 --- /dev/null +++ b/.gitlab/merge_request_templates/Change documentation location.md @@ -0,0 +1,32 @@ +<!--See the general Documentation guidelines https://docs.gitlab.com/ee/development/documentation/ --> + +<!-- Use this description template for changing documentation location. For new docs or updates to existing docs, use the "Documentation" template --> + +## What does this MR do? + +<!-- Briefly describe what this MR is about --> + +## Related issues + +<!-- Mention the issue(s) this MR closes or is related to --> + +Closes + +## Moving docs to a new location? + +Read the guidelines: +https://docs.gitlab.com/ce/development/documentation/index.html#changing-document-location + +- [ ] Make sure the old link is not removed and has its contents replaced with + a link to the new location. +- [ ] Make sure internal links pointing to the document in question are not broken. +- [ ] Search and replace any links referring to old docs in GitLab Rails app, + specifically under the `app/views/` and `ee/app/views` (for GitLab EE) directories. +- [ ] Make sure to add [`redirect_from`](https://docs.gitlab.com/ce/development/writing_documentation.html#redirections-for-pages-with-disqus-comments) + to the new document if there are any Disqus comments on the old document thread. +- [ ] Update the link in `features.yml` (if applicable) +- [ ] If working on CE and the `ee-compat-check` jobs fails, submit an MR to EE + with the changes as well (https://docs.gitlab.com/ce/development/writing_documentation.html#cherry-picking-from-ce-to-ee). +- [ ] Ping one of the technical writers for review. + +/label ~Documentation diff --git a/.gitlab/merge_request_templates/Documentation.md b/.gitlab/merge_request_templates/Documentation.md index 531035b3766..ca38c881c66 100644 --- a/.gitlab/merge_request_templates/Documentation.md +++ b/.gitlab/merge_request_templates/Documentation.md @@ -1,4 +1,8 @@ -<!--See the general Documentation guidelines https://docs.gitlab.com/ee/development/documentation/index.html --> +<!--See the general documentation guidelines https://docs.gitlab.com/ee/development/documentation --> + +<!-- Mention "documentation" or "docs" in the MR title --> + +<!-- Use this description template for new docs or updates to existing docs. For changing documentation location use the "Change documentation location" template --> ## What does this MR do? @@ -10,20 +14,19 @@ Closes -## Moving docs to a new location? - -Read the guidelines: -https://docs.gitlab.com/ee/development/documentation/#changing-document-location - -- [ ] Make sure the old link is not removed and has its contents replaced with - a link to the new location. -- [ ] Make sure internal links pointing to the document in question are not broken. -- [ ] Search and replace any links referring to the old docs in the GitLab Rails app, - specifically under the `app/views/` and `ee/app/views` (for GitLab EE) directories. -- [ ] Make sure to add [`redirect_from`](https://docs.gitlab.com/ee/development/documentation/index.html#redirections-for-pages-with-disqus-comments) - to the new document if there are any Disqus comments on the old document thread. -- [ ] If working on CE and the `ee-compat-check` jobs fails, [submit an MR to EE - with the changes](https://docs.gitlab.com/ee/development/documentation/index.html#cherry-picking-from-ce-to-ee) as well. -- [ ] Ping one of the technical writers for review. +## Author's checklist + +- [ ] [Apply the correct labels and milestone](https://docs.gitlab.com/ee/development/documentation/workflow.html#2-developer-s-role-in-the-documentation-process) +- [ ] Crosslink the document from the higher-level index +- [ ] Crosslink the document from other subject-related docs +- [ ] Correctly apply the product [badges](https://docs.gitlab.com/ee/development/documentation/styleguide.html#product-badges) and [tiers](https://docs.gitlab.com/ee/development/documentation/styleguide.html#gitlab-versions-and-tiers) +- [ ] [Port the MR to EE (or backport from CE)](https://docs.gitlab.com/ee/development/documentation/index.html#cherry-picking-from-ce-to-ee): _always recommended, required when the `ee-compat-check` job fails_ + +## Review checklist + +- [ ] Your team's review (required) +- [ ] PM's review (recommended, but not a blocker) +- [ ] Technical writer's review (required) +- [ ] Merge the EE-MR first, CE-MR afterwards /label ~Documentation diff --git a/.rubocop.yml b/.rubocop.yml index c8b1ce327e2..9858bbe0ddd 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -31,6 +31,10 @@ Style/MutableConstant: - 'ee/db/post_migrate/**/*' - 'ee/db/geo/migrate/**/*' +# TODO: Move this to gitlab-styles +Style/SafeNavigation: + Enabled: false + Naming/FileName: ExpectMatchingDefinition: true Exclude: @@ -44,6 +48,8 @@ Naming/FileName: - 'qa/bin/*' - 'config/**/*' - 'lib/generators/**/*' + - 'locale/unfound_translations.rb' + - 'ee/locale/unfound_translations.rb' - 'ee/lib/generators/**/*' IgnoreExecutableScripts: true AllowedAcronyms: diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 8a1ca6747a8..54e3b8217d8 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -706,12 +706,6 @@ Style/RescueModifier: Style/RescueStandardError: Enabled: false -# Offense count: 92 -# Cop supports --auto-correct. -# Configuration parameters: ConvertCodeThatCanStartToReturnNil. -Style/SafeNavigation: - Enabled: false - # Offense count: 8 # Cop supports --auto-correct. Style/SelfAssignment: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e68e3b9cab0..fb7c0c88629 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -64,11 +64,11 @@ As of July 2018, all the documentation for contributing to the GitLab project ha ## Contribute to GitLab -For a first-time step-by-step guide to the contribution process, see -["Contributing to GitLab"](https://about.gitlab.com/contributing/). - Thank you for your interest in contributing to GitLab. This guide details how -to contribute to GitLab in a way that is efficient for everyone. +to contribute to GitLab in a way that is easy for everyone. + +For a first-time step-by-step guide to the contribution process, please see +["Contributing to GitLab"](https://about.gitlab.com/contributing/). Looking for something to work on? Look for issues with the label [Accepting Merge Requests](#i-want-to-contribute). @@ -77,10 +77,10 @@ source edition, and GitLab Enterprise Edition (EE) which is our commercial edition. Throughout this guide you will see references to CE and EE for abbreviation. -If you have read this guide and want to know how the GitLab [core team] +If you want to know how the GitLab [core team] operates please see [the GitLab contributing process](PROCESS.md). -- [GitLab Inc engineers should refer to the engineering workflow document](https://about.gitlab.com/handbook/engineering/workflow/) +[GitLab Inc engineers should refer to the engineering workflow document](https://about.gitlab.com/handbook/engineering/workflow/) ## Security vulnerability disclosure diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index a38b3bd31b1..377d8aca07e 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -0.117.0 +0.117.2 diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION index 8104cabd36f..0e79152459e 100644 --- a/GITLAB_SHELL_VERSION +++ b/GITLAB_SHELL_VERSION @@ -1 +1 @@ -8.1.0 +8.1.1 diff --git a/GITLAB_WORKHORSE_VERSION b/GITLAB_WORKHORSE_VERSION index 831446cbd27..09b254e90c6 100644 --- a/GITLAB_WORKHORSE_VERSION +++ b/GITLAB_WORKHORSE_VERSION @@ -1 +1 @@ -5.1.0 +6.0.0 @@ -180,7 +180,7 @@ gem 'rufus-scheduler', '~> 3.4' gem 'httparty', '~> 0.13.3' # Colored output to console -gem 'rainbow', '~> 2.2' +gem 'rainbow', '~> 3.0' # Progress bar gem 'ruby-progressbar' @@ -214,9 +214,6 @@ gem 'jira-ruby', '~> 1.4' # Flowdock integration gem 'gitlab-flowdock-git-hook', '~> 1.0.1' -# Gemnasium integration -gem 'gemnasium-gitlab-service', '~> 0.2' - # Slack integration gem 'slack-notifier', '~> 1.5.1' @@ -423,7 +420,7 @@ group :ed25519 do end # Gitaly GRPC client -gem 'gitaly-proto', '~> 0.112.0', require: 'gitaly' +gem 'gitaly-proto', '~> 0.113.0', require: 'gitaly' gem 'grpc', '~> 1.11.0' # Locked until https://github.com/google/protobuf/issues/4210 is closed diff --git a/Gemfile.lock b/Gemfile.lock index 1537cacaadd..15a105579fb 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -123,7 +123,7 @@ GEM numerizer (~> 0.1.1) chunky_png (1.3.5) citrus (3.0.2) - coderay (1.1.1) + coderay (1.1.2) coercible (1.0.0) descendants_tracker (~> 0.0.1) commonmarker (0.17.8) @@ -269,8 +269,6 @@ GEM fuubar (2.2.0) rspec-core (~> 3.0) ruby-progressbar (~> 1.4) - gemnasium-gitlab-service (0.2.6) - rugged (~> 0.21) gemojione (3.3.0) json get_process_mem (0.2.0) @@ -284,7 +282,7 @@ GEM gettext_i18n_rails (>= 0.7.1) po_to_json (>= 1.0.0) rails (>= 3.2.0) - gitaly-proto (0.112.0) + gitaly-proto (0.113.0) google-protobuf (~> 3.1) grpc (~> 1.10) github-linguist (5.3.3) @@ -494,7 +492,7 @@ GEM memoist (0.16.0) memoizable (0.4.2) thread_safe (~> 0.3, >= 0.3.1) - method_source (0.8.2) + method_source (0.9.0) mime-types (3.1) mime-types-data (~> 3.2015) mime-types-data (3.2016.0521) @@ -636,12 +634,11 @@ GEM unparser procto (0.0.3) prometheus-client-mmap (0.9.4) - pry (0.10.4) + pry (0.11.3) coderay (~> 1.1.0) - method_source (~> 0.8.1) - slop (~> 3.4) - pry-byebug (3.4.2) - byebug (~> 9.0) + method_source (~> 0.9.0) + pry-byebug (3.4.3) + byebug (>= 9.0, < 9.1) pry (~> 0.10) pry-rails (0.3.5) pry (>= 0.9.10) @@ -692,8 +689,7 @@ GEM activesupport (= 4.2.10) rake (>= 0.8.7) thor (>= 0.18.1, < 2.0) - rainbow (2.2.2) - rake + rainbow (3.0.0) raindrops (0.18.0) rake (12.3.1) rb-fsevent (0.10.2) @@ -745,7 +741,7 @@ GEM retriable (3.1.1) rinku (2.0.0) rotp (2.1.2) - rouge (3.2.0) + rouge (3.2.1) rqrcode (0.7.0) chunky_png rqrcode-rails3 (0.1.7) @@ -811,7 +807,7 @@ GEM rubyzip (1.2.1) rufus-scheduler (3.4.0) et-orbi (~> 1.0) - rugged (0.27.2) + rugged (0.27.4) safe_yaml (1.0.4) sanitize (4.6.6) crass (~> 1.0.2) @@ -872,7 +868,6 @@ GEM simplecov-html (~> 0.10.0) simplecov-html (0.10.0) slack-notifier (1.5.1) - slop (3.6.0) spring (2.0.1) activesupport (>= 4.2) spring-commands-rspec (1.0.4) @@ -1043,12 +1038,11 @@ DEPENDENCIES font-awesome-rails (~> 4.7) foreman (~> 0.84.0) fuubar (~> 2.2.0) - gemnasium-gitlab-service (~> 0.2) gemojione (~> 3.3) gettext (~> 3.2.2) gettext_i18n_rails (~> 1.8.0) gettext_i18n_rails_js (~> 1.3) - gitaly-proto (~> 0.112.0) + gitaly-proto (~> 0.113.0) github-linguist (~> 5.3.3) gitlab-flowdock-git-hook (~> 1.0.1) gitlab-gollum-lib (~> 4.2) @@ -1136,7 +1130,7 @@ DEPENDENCIES rails (= 4.2.10) rails-deprecated_sanitizer (~> 1.0.3) rails-i18n (~> 4.0.9) - rainbow (~> 2.2) + rainbow (~> 3.0) raindrops (~> 0.18) rblineprof (~> 0.3.6) rbtrace (~> 0.4) @@ -1207,4 +1201,4 @@ DEPENDENCIES wikicloth (= 0.8.1) BUNDLED WITH - 1.16.2 + 1.16.3 diff --git a/Gemfile.rails5.lock b/Gemfile.rails5.lock index 39305927c0f..7803d12c6b4 100644 --- a/Gemfile.rails5.lock +++ b/Gemfile.rails5.lock @@ -272,8 +272,6 @@ GEM fuubar (2.2.0) rspec-core (~> 3.0) ruby-progressbar (~> 1.4) - gemnasium-gitlab-service (0.2.6) - rugged (~> 0.21) gemojione (3.3.0) json get_process_mem (0.2.0) @@ -287,7 +285,7 @@ GEM gettext_i18n_rails (>= 0.7.1) po_to_json (>= 1.0.0) rails (>= 3.2.0) - gitaly-proto (0.112.0) + gitaly-proto (0.113.0) google-protobuf (~> 3.1) grpc (~> 1.10) github-linguist (5.3.3) @@ -701,8 +699,7 @@ GEM method_source rake (>= 0.8.7) thor (>= 0.18.1, < 2.0) - rainbow (2.2.2) - rake + rainbow (3.0.0) raindrops (0.18.0) rake (12.3.1) rb-fsevent (0.10.2) @@ -1053,12 +1050,11 @@ DEPENDENCIES font-awesome-rails (~> 4.7) foreman (~> 0.84.0) fuubar (~> 2.2.0) - gemnasium-gitlab-service (~> 0.2) gemojione (~> 3.3) gettext (~> 3.2.2) gettext_i18n_rails (~> 1.8.0) gettext_i18n_rails_js (~> 1.3) - gitaly-proto (~> 0.112.0) + gitaly-proto (~> 0.113.0) github-linguist (~> 5.3.3) gitlab-flowdock-git-hook (~> 1.0.1) gitlab-gollum-lib (~> 4.2) @@ -1147,7 +1143,7 @@ DEPENDENCIES rails-controller-testing rails-deprecated_sanitizer (~> 1.0.3) rails-i18n (~> 5.1) - rainbow (~> 2.2) + rainbow (~> 3.0) raindrops (~> 0.18) rblineprof (~> 0.3.6) rbtrace (~> 0.4) @@ -1217,4 +1213,4 @@ DEPENDENCIES wikicloth (= 0.8.1) BUNDLED WITH - 1.16.2 + 1.16.3 diff --git a/PROCESS.md b/PROCESS.md index a06ddb68b77..583f36b820f 100644 --- a/PROCESS.md +++ b/PROCESS.md @@ -15,8 +15,9 @@ - [Between the 1st and the 7th](#between-the-1st-and-the-7th) - [On the 7th](#on-the-7th) - [After the 7th](#after-the-7th) -- [Regressions](#regressions) - - [How to manage a regression](#how-to-manage-a-regression) +- [Bugs](#bugs) + - [Regressions](#regressions) + - [Managing bugs](#managing-bugs) - [Release retrospective and kickoff](#release-retrospective-and-kickoff) - [Retrospective](#retrospective) - [Kickoff](#kickoff) @@ -115,6 +116,11 @@ target. However, if one does and falls into either of the above categories, it's the reviewer's responsibility to manage the above communication and assignment on behalf of the community member. +Every new feature or change should be shipped with its corresponding documentation +in accordance with the +[documentation process](https://docs.gitlab.com/ee/development/documentation/workflow.html) +and [structure](https://docs.gitlab.com/ee/development/documentation/structure.html). + #### What happens if these deadlines are missed? If a small or large feature is _not_ with a maintainer or reviewer by the @@ -140,14 +146,9 @@ and to prevent any last minute surprises. ### On the 7th -Merge requests should still be complete, following the -[definition of done][done]. The single exception is documentation, and this can -only be left until after the freeze if: +Merge requests should still be complete, following the [definition of done][done]. -* There is a follow-up issue to add documentation. -* It is assigned to the person writing documentation for this feature, and they - are aware of it. -* It is in the correct milestone, with the ~Deliverable label. +#### Feature merge requests If a merge request is not ready, but the developers and Product Manager responsible for the feature think it is essential that it is in the release, @@ -163,15 +164,32 @@ information, see [Automatic CE->EE merge][automatic_ce_ee_merge] and [Guidelines for implementing Enterprise Edition features][ee_features]. +#### Documentation merge requests + +Documentation is part of the product and must be shipped with the feature. + +The single exception for the feature freeze is documentation, and it can +be left to be **merged up to the 14th** if: + +* There is a follow-up issue to add documentation. +* It is assigned to the developer writing documentation for this feature, and they + are aware of it. +* It is in the correct milestone, with the labels ~Documentation, ~Deliverable, +~missed-deliverable, and "pick into X.Y" applied. +* It must be reviewed and approved by a technical writer. + +For more information read the process for +[documentation shipped late](https://docs.gitlab.com/ee/development/documentation/workflow.html#documentation-shipped-late). + ### After the 7th Once the stable branch is frozen, the only MRs that can be cherry-picked into the stable branch are: -* Fixes for [regressions](#regressions) +* Fixes for [regressions](#regressions) where the affected version `xx.x` in `regression:xx.x` is the current release. See [Managing bugs](#managing-bugs) section. * Fixes for security issues * Fixes or improvements to automated QA scenarios -* Documentation updates for changes in the same release +* [Documentation updates](https://docs.gitlab.com/ee/development/documentation/workflow.html#documentation-shipped-late) for changes in the same release * New or updated translations (as long as they do not touch application code) During the feature freeze all merge requests that are meant to go into the @@ -201,48 +219,59 @@ you can ask for an exception to be made. Check [this guide](https://gitlab.com/gitlab-org/release/docs/blob/master/general/exception-request/process.md) about how to open an exception request before opening one. -## Regressions +## Bugs + +A ~bug is a defect, error, failure which causes the system to behave incorrectly or prevents it from fulfilling the product requirements. + +The level of impact of a ~bug can vary from blocking a whole functionality +or a feature usability bug. A bug should always be linked to a severity level. +Refer to our [severity levels](../CONTRIBUTING.md#severity-labels) + +Whether the bug is also a regression or not, the triage process should start as soon as possible. +Ensure that the Engineering Manager and/or the Product Manager for the relative area is involved to prioritize the work as needed. + +### Regressions + +A ~regression implies that a previously **verified working functionality** no longer works. +Regressions are a subset of bugs. We use the ~regression label to imply that the defect caused the functionality to regress. +The label tells us that something worked before and it needs extra attention from Engineering and Product Managers to schedule/reschedule. -A regression for a particular monthly release is a bug that exists in that -release, but wasn't present in the release before. This includes bugs in -features that were only added in that monthly release. Every regression **must** -have the milestone of the release it was introduced in - if a regression doesn't -have a milestone, it might be 'just' a bug! +The regression label does not apply to ~bugs for new features for which functionality was **never verified as working**. +These, by definition, are not regressions. -For instance, if 10.5.0 adds a feature, and that feature doesn't work correctly, -then this is a regression in 10.5. If 10.5.1 then fixes that, but 10.5.3 somehow -reintroduces the bug, then this bug is still a regression in 10.5. +A regression should always have the `regression:xx.x` label on it to designate when it was introduced. -Because GitLab.com runs release candidates of new releases, a regression can be -reported in a release before its 'official' release date on the 22nd of the -month. When we say 'the most recent monthly release', this can refer to either -the version currently running on GitLab.com, or the most recent version -available in the package repositories. +Regressions should be considered high priority issues that should be solved as soon as possible, especially if they have severe impact on users. -### How to manage a regression +### Managing bugs -Regressions are very important, and they should be considered high priority -issues that should be solved as soon as possible, especially if they affect -users. Despite that, ~regression label itself does not imply when the issue -will be scheduled. +**Prioritization:** We give higher priority to regressions on features that worked in the last recent monthly release and the current release candidates. +The two scenarios below can [bypass the exception request in the release process](https://gitlab.com/gitlab-org/release/docs/blob/master/general/exception-request/process.md#after-the-7th), where the affected regression version matches the current monthly release version. +* A regression which worked in the **Last monthly release** + * **Example:** In 11.0 we released a new `feature X` that is verified as working. Then in release 11.1 the feature no longer works, this is regression for 11.1. The issue should have the `regression:11.1` label. + * *Note:* When we say `the last recent monthly release`, this can refer to either the version currently running on GitLab.com, or the most recent version available in the package repositories. +* A regression which worked in the **Current release candidates** + * **Example:** In 11.1-RC3 we shipped a new feature which has been verified as working. Then in 11.1-RC5 the feature no longer works, this is regression for 11.1. The issue should have the `regression:11.1` label. + * *Note:* Because GitLab.com runs release candidates of new releases, a regression can be reported in a release before its 'official' release date on the 22nd of the month. -When a regression is found: -1. Create an issue describing the problem in the most detailed way possible -1. If possible, provide links to real examples and how to reproduce the problem +When a bug is found: +1. Create an issue describing the problem in the most detailed way possible. +1. If possible, provide links to real examples and how to reproduce the problem. 1. Label the issue properly, using the [team label](../CONTRIBUTING.md#team-labels), the [subject label](../CONTRIBUTING.md#subject-labels) and any other label that may apply in the specific case -1. Add the ~bug and ~regression labels -1. Notify the respective Engineering Manager to evaluate the Severity of the regression and add a [Severity label](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#bug-severity-labels). The counterpart Product Manager is included to weigh-in on prioritization as needed to set the [Priority label](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#bug-priority-labels). -1. If the regression is either an ~S1, ~S2 or ~S3 severity, label the regression with the current milestone as it should be fixed in the current milestone. - 1. If the regression was introduced in an RC of the current release, label with ~Deliverable - 1. If the regression was introduced in the previous release, label with ~"Next Patch Release" -1. If the regression is an ~S4 severity, the regression may be scheduled for later milestones at the discretion of Engineering Manager and Product Manager. - -When a new issue is found, the fix should start as soon as possible. You can -ping the Engineering Manager or the Product Manager for the relative area to -make them aware of the issue earlier. They will analyze the priority and change -it if needed. +1. Notify the respective Engineering Manager to evaluate and apply the [Severity label](../CONTRIBUTING.md#bug-severity-labels) and [Priority label](../CONTRIBUTING.md#bug-priority-labels). +The counterpart Product Manager is included to weigh-in on prioritization as needed. +1. If the ~bug is **NOT** a regression: + 1. The Engineering Manager decides which milestone the bug will be fixed. The appropriate milestone is applied. +1. If the bug is a ~regression: + 1. Determine the release that the regression affects and add the corresponding `regression:xx.x` label. + 1. If the affected release version can't be determined, add the generic ~regression label for the time being. + 1. If the affected version `xx.x` in `regression:xx.x` is the **current release**, it's recommended to schedule the fix for the current milestone. + 1. This falls under regressions which worked in the last release and the current RCs. More detailed explanations in the **Prioritization** section above. + 1. If the affected version `xx.x` in `regression:xx.x` is older than the **current release** + 1. If the regression is an ~S1 severity, it's recommended to schedule the fix for the current milestone. We would like to fix the highest severity regression as soon as we can. + 1. If the regression is an ~S2, ~S3 or ~S4 severity, the regression may be scheduled for later milestones at the discretion of the Engineering Manager and Product Manager. ## Release retrospective and kickoff diff --git a/README.md b/README.md index b6e1cc9a432..335736e53f5 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,7 @@ You can access a new installation with the login **`root`** and password **`5ive ## Contributing -GitLab is an open source project and we are very happy to accept community contributions. Please refer to [CONTRIBUTING.md](/CONTRIBUTING.md) for details. +GitLab is an open source project and we are very happy to accept community contributions. Please refer to [Contributing to GitLab page](https://about.gitlab.com/contributing/) for more details. ## Licensing @@ -66,7 +66,7 @@ GitLab Community Edition (CE) is available freely under the MIT Expat license. All third party components incorporated into the GitLab Software are licensed under the original license provided by the owner of the applicable component. -All Documentation content that resides under the doc/ directory of this repository is licensed under Creative Commons: CC BY-SA 4.0. +All Documentation content that resides under the `doc/` directory of this repository is licensed under Creative Commons: CC BY-SA 4.0. ## Install a development environment diff --git a/app/assets/javascripts/boards/components/board_list.vue b/app/assets/javascripts/boards/components/board_list.vue index 3e610a4088c..bfc8d9b03ad 100644 --- a/app/assets/javascripts/boards/components/board_list.vue +++ b/app/assets/javascripts/boards/components/board_list.vue @@ -203,7 +203,7 @@ export default { this.showIssueForm = !this.showIssueForm; }, onScroll() { - if (!this.loadingMore && (this.scrollTop() > this.scrollHeight() - this.scrollOffset)) { + if (!this.list.loadingMore && (this.scrollTop() > this.scrollHeight() - this.scrollOffset)) { this.loadNextPage(); } }, diff --git a/app/assets/javascripts/boards/components/board_sidebar.js b/app/assets/javascripts/boards/components/board_sidebar.js index a9102743bf9..109e60cbde2 100644 --- a/app/assets/javascripts/boards/components/board_sidebar.js +++ b/app/assets/javascripts/boards/components/board_sidebar.js @@ -3,7 +3,7 @@ import $ from 'jquery'; import Vue from 'vue'; import Flash from '../../flash'; -import { __ } from '../../locale'; +import { sprintf, __ } from '../../locale'; import Sidebar from '../../right_sidebar'; import eventHub from '../../sidebar/event_hub'; import AssigneeTitle from '../../sidebar/components/assignees/assignee_title.vue'; @@ -55,8 +55,10 @@ gl.issueBoards.BoardSidebar = Vue.extend({ return this.issue.labels && this.issue.labels.length; }, labelDropdownTitle() { - return this.hasLabels ? - `${this.issue.labels[0].title} ${this.issue.labels.length - 1}+ more` : 'Label'; + return this.hasLabels ? sprintf(__('%{firstLabel} +%{labelCount} more'), { + firstLabel: this.issue.labels[0].title, + labelCount: this.issue.labels.length - 1 + }) : __('Label'); }, selectedLabels() { return this.hasLabels ? this.issue.labels.map(l => l.title).join(',') : ''; diff --git a/app/assets/javascripts/boards/models/list.js b/app/assets/javascripts/boards/models/list.js index 050cbd8db48..ad473404c29 100644 --- a/app/assets/javascripts/boards/models/list.js +++ b/app/assets/javascripts/boards/models/list.js @@ -1,6 +1,7 @@ /* eslint-disable no-underscore-dangle, class-methods-use-this, consistent-return, no-shadow, no-param-reassign, max-len */ /* global ListIssue */ +import { __ } from '~/locale'; import ListLabel from '~/vue_shared/models/label'; import ListAssignee from '~/vue_shared/models/assignee'; import queryData from '../utils/query_data'; @@ -30,7 +31,7 @@ class List { this.id = obj.id; this._uid = this.guid(); this.position = obj.position; - this.title = obj.title; + this.title = obj.list_type === 'backlog' ? __('Open') : obj.title; this.type = obj.list_type; const typeInfo = this.getTypeInfo(this.type); diff --git a/app/assets/javascripts/commons/polyfills.js b/app/assets/javascripts/commons/polyfills.js index 589eeee9695..742cf490ad2 100644 --- a/app/assets/javascripts/commons/polyfills.js +++ b/app/assets/javascripts/commons/polyfills.js @@ -8,6 +8,7 @@ import 'core-js/fn/object/assign'; import 'core-js/fn/promise'; import 'core-js/fn/string/code-point-at'; import 'core-js/fn/string/from-code-point'; +import 'core-js/fn/string/includes'; import 'core-js/fn/symbol'; import 'core-js/es6/map'; import 'core-js/es6/weak-map'; diff --git a/app/assets/javascripts/diffs/components/app.vue b/app/assets/javascripts/diffs/components/app.vue index 7cc4e6a2c3a..b5b05df4d34 100644 --- a/app/assets/javascripts/diffs/components/app.vue +++ b/app/assets/javascripts/diffs/components/app.vue @@ -114,11 +114,15 @@ export default { this.adjustView(); }, methods: { - ...mapActions('diffs', ['setBaseConfig', 'fetchDiffFiles']), + ...mapActions('diffs', ['setBaseConfig', 'fetchDiffFiles', 'startRenderDiffsQueue']), fetchData() { - this.fetchDiffFiles().catch(() => { - createFlash(__('Something went wrong on our end. Please try again!')); - }); + this.fetchDiffFiles() + .then(() => { + requestIdleCallback(this.startRenderDiffsQueue, { timeout: 1000 }); + }) + .catch(() => { + createFlash(__('Something went wrong on our end. Please try again!')); + }); if (!this.isNotesFetched) { eventHub.$emit('fetchNotesData'); diff --git a/app/assets/javascripts/diffs/components/diff_file.vue b/app/assets/javascripts/diffs/components/diff_file.vue index 7e7058d8d08..59e9ba08b8b 100644 --- a/app/assets/javascripts/diffs/components/diff_file.vue +++ b/app/assets/javascripts/diffs/components/diff_file.vue @@ -46,16 +46,25 @@ export default { showExpandMessage() { return this.isCollapsed && !this.isLoadingCollapsedDiff && !this.file.tooLarge; }, + showLoadingIcon() { + return this.isLoadingCollapsedDiff || (!this.file.renderIt && !this.isCollapsed); + }, }, methods: { ...mapActions('diffs', ['loadCollapsedDiff']), handleToggle() { const { collapsed, highlightedDiffLines, parallelDiffLines } = this.file; - if (collapsed && !highlightedDiffLines && !parallelDiffLines.length) { + if ( + collapsed && + !highlightedDiffLines && + parallelDiffLines !== undefined && + !parallelDiffLines.length + ) { this.handleLoadCollapsedDiff(); } else { this.file.collapsed = !this.file.collapsed; + this.file.renderIt = true; } }, handleLoadCollapsedDiff() { @@ -65,6 +74,7 @@ export default { .then(() => { this.isLoadingCollapsedDiff = false; this.file.collapsed = false; + this.file.renderIt = true; }) .catch(() => { this.isLoadingCollapsedDiff = false; @@ -121,12 +131,12 @@ export default { </div> <diff-content - v-if="!isCollapsed" + v-if="!isCollapsed && file.renderIt" :class="{ hidden: isCollapsed || file.tooLarge }" :diff-file="file" /> <loading-icon - v-if="isLoadingCollapsedDiff" + v-else-if="showLoadingIcon" class="diff-content loading" /> <div diff --git a/app/assets/javascripts/diffs/constants.js b/app/assets/javascripts/diffs/constants.js index 2fa8367f528..f68afa44837 100644 --- a/app/assets/javascripts/diffs/constants.js +++ b/app/assets/javascripts/diffs/constants.js @@ -25,3 +25,6 @@ export const CONTEXT_LINE_CLASS_NAME = 'diff-expanded'; export const UNFOLD_COUNT = 20; export const COUNT_OF_AVATARS_IN_GUTTER = 3; export const LENGTH_OF_AVATAR_TOOLTIP = 17; + +export const LINES_TO_BE_RENDERED_DIRECTLY = 100; +export const MAX_LINES_TO_BE_RENDERED = 2000; diff --git a/app/assets/javascripts/diffs/store/actions.js b/app/assets/javascripts/diffs/store/actions.js index 27001142257..4ab6ceb249a 100644 --- a/app/assets/javascripts/diffs/store/actions.js +++ b/app/assets/javascripts/diffs/store/actions.js @@ -29,6 +29,27 @@ export const fetchDiffFiles = ({ state, commit }) => { .then(handleLocationHash); }; +export const startRenderDiffsQueue = ({ state, commit }) => { + const checkItem = () => { + const nextFile = state.diffFiles.find( + file => !file.renderIt && (!file.collapsed || !file.text), + ); + if (nextFile) { + requestAnimationFrame(() => { + commit(types.RENDER_FILE, nextFile); + }); + requestIdleCallback( + () => { + checkItem(); + }, + { timeout: 1000 }, + ); + } + }; + + checkItem(); +}; + export const setInlineDiffViewType = ({ commit }) => { commit(types.SET_DIFF_VIEW_TYPE, INLINE_DIFF_VIEW_TYPE); diff --git a/app/assets/javascripts/diffs/store/mutation_types.js b/app/assets/javascripts/diffs/store/mutation_types.js index 2c8e1a1466f..c999d637d50 100644 --- a/app/assets/javascripts/diffs/store/mutation_types.js +++ b/app/assets/javascripts/diffs/store/mutation_types.js @@ -8,3 +8,4 @@ export const REMOVE_COMMENT_FORM_LINE = 'REMOVE_COMMENT_FORM_LINE'; export const ADD_CONTEXT_LINES = 'ADD_CONTEXT_LINES'; export const ADD_COLLAPSED_DIFFS = 'ADD_COLLAPSED_DIFFS'; export const EXPAND_ALL_FILES = 'EXPAND_ALL_FILES'; +export const RENDER_FILE = 'RENDER_FILE'; diff --git a/app/assets/javascripts/diffs/store/mutations.js b/app/assets/javascripts/diffs/store/mutations.js index a98b2be89a3..0522e32c410 100644 --- a/app/assets/javascripts/diffs/store/mutations.js +++ b/app/assets/javascripts/diffs/store/mutations.js @@ -2,6 +2,7 @@ import Vue from 'vue'; import _ from 'underscore'; import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; import { findDiffFile, addLineReferences, removeMatchLine, addContextLines } from './utils'; +import { LINES_TO_BE_RENDERED_DIRECTLY, MAX_LINES_TO_BE_RENDERED } from '../constants'; import * as types from './mutation_types'; export default { @@ -15,8 +16,48 @@ export default { }, [types.SET_DIFF_DATA](state, data) { + const diffData = convertObjectPropsToCamelCase(data, { deep: true }); + let showingLines = 0; + const filesLength = diffData.diffFiles.length; + let i; + for (i = 0; i < filesLength; i += 1) { + const file = diffData.diffFiles[i]; + if (file.parallelDiffLines) { + const linesLength = file.parallelDiffLines.length; + let u = 0; + for (u = 0; u < linesLength; u += 1) { + const line = file.parallelDiffLines[u]; + if (line.left) delete line.left.text; + if (line.right) delete line.right.text; + } + } + + if (file.highlightedDiffLines) { + const linesLength = file.highlightedDiffLines.length; + let u; + for (u = 0; u < linesLength; u += 1) { + const line = file.highlightedDiffLines[u]; + delete line.text; + } + } + + if (file.highlightedDiffLines) { + showingLines += file.parallelDiffLines.length; + } + Object.assign(file, { + renderIt: showingLines < LINES_TO_BE_RENDERED_DIRECTLY, + collapsed: file.text && showingLines > MAX_LINES_TO_BE_RENDERED, + }); + } + Object.assign(state, { - ...convertObjectPropsToCamelCase(data, { deep: true }), + ...diffData, + }); + }, + + [types.RENDER_FILE](state, file) { + Object.assign(file, { + renderIt: true, }); }, diff --git a/app/assets/javascripts/emoji/support/unicode_support_map.js b/app/assets/javascripts/emoji/support/unicode_support_map.js index 8c1861c56db..651169391fe 100644 --- a/app/assets/javascripts/emoji/support/unicode_support_map.js +++ b/app/assets/javascripts/emoji/support/unicode_support_map.js @@ -86,7 +86,7 @@ function generateUnicodeSupportMap(testMap) { canvas.height = numTestEntries * fontSize; ctx.fillStyle = '#000000'; ctx.textBaseline = 'middle'; - ctx.font = `${fontSize}px "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"`; + ctx.font = `${fontSize}px "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"`; // Write each emoji to the canvas vertically let writeIndex = 0; testMapKeys.forEach(testKey => { diff --git a/app/assets/javascripts/ide/components/ide_tree_list.vue b/app/assets/javascripts/ide/components/ide_tree_list.vue index 5611b37be7c..00ae5ea2c15 100644 --- a/app/assets/javascripts/ide/components/ide_tree_list.vue +++ b/app/assets/javascripts/ide/components/ide_tree_list.vue @@ -61,7 +61,7 @@ export default { <slot name="header"></slot> </header> <div - class="ide-tree-body" + class="ide-tree-body h-100" > <repo-file v-for="file in currentTree.tree" diff --git a/app/assets/javascripts/ide/components/new_dropdown/button.vue b/app/assets/javascripts/ide/components/new_dropdown/button.vue index ff114e47741..aa5fce59dbf 100644 --- a/app/assets/javascripts/ide/components/new_dropdown/button.vue +++ b/app/assets/javascripts/ide/components/new_dropdown/button.vue @@ -1,7 +1,11 @@ <script> import Icon from '~/vue_shared/components/icon.vue'; +import tooltip from '~/vue_shared/directives/tooltip'; export default { + directives: { + tooltip, + }, components: { Icon, }, @@ -26,6 +30,11 @@ export default { default: true, }, }, + computed: { + tooltipTitle() { + return this.showLabel ? '' : this.label; + }, + }, methods: { clicked() { this.$emit('click'); @@ -36,7 +45,9 @@ export default { <template> <button + v-tooltip :aria-label="label" + :title="tooltipTitle" type="button" class="btn-blank" @click.stop.prevent="clicked" diff --git a/app/assets/javascripts/ide/components/preview/clientside.vue b/app/assets/javascripts/ide/components/preview/clientside.vue index fef36eae7b1..39a1bd1f61b 100644 --- a/app/assets/javascripts/ide/components/preview/clientside.vue +++ b/app/assets/javascripts/ide/components/preview/clientside.vue @@ -2,6 +2,7 @@ import { mapActions, mapGetters, mapState } from 'vuex'; import _ from 'underscore'; import { Manager } from 'smooshpack'; +import { listen } from 'codesandbox-api'; import LoadingIcon from '~/vue_shared/components/loading_icon.vue'; import Navigator from './navigator.vue'; import { packageJsonPath } from '../../constants'; @@ -16,6 +17,7 @@ export default { return { manager: {}, loading: false, + sandpackReady: false, }; }, computed: { @@ -81,6 +83,10 @@ export default { } this.manager = {}; + if (this.listener) { + this.listener(); + } + clearTimeout(this.timeout); this.timeout = null; }, @@ -96,17 +102,29 @@ export default { return this.loadFileContent(this.mainEntry) .then(() => this.$nextTick()) - .then(() => + .then(() => { this.initManager('#ide-preview', this.sandboxOpts, { fileResolver: { isFile: p => Promise.resolve(!!this.entries[createPathWithExt(p)]), readFile: p => this.loadFileContent(createPathWithExt(p)).then(content => content), }, - }), - ); + }); + + this.listener = listen(e => { + switch (e.type) { + case 'done': + this.sandpackReady = true; + break; + default: + break; + } + }); + }); }, update() { - if (this.timeout) return; + if (!this.sandpackReady) return; + + clearTimeout(this.timeout); this.timeout = setTimeout(() => { if (_.isEmpty(this.manager)) { @@ -116,10 +134,7 @@ export default { } this.manager.updatePreview(this.sandboxOpts); - - clearTimeout(this.timeout); - this.timeout = null; - }, 500); + }, 250); }, initManager(el, opts, resolver) { this.manager = new Manager(el, opts, resolver); diff --git a/app/assets/javascripts/ide/components/repo_editor.vue b/app/assets/javascripts/ide/components/repo_editor.vue index f9badb01535..f55aa843444 100644 --- a/app/assets/javascripts/ide/components/repo_editor.vue +++ b/app/assets/javascripts/ide/components/repo_editor.vue @@ -133,7 +133,6 @@ export default { .then(() => this.getRawFileData({ path: this.file.path, - baseSha: this.currentMergeRequest ? this.currentMergeRequest.baseCommitSha : '', }), ) .then(() => { diff --git a/app/assets/javascripts/ide/ide_router.js b/app/assets/javascripts/ide/ide_router.js index c6d7d218e81..3f6101e58f4 100644 --- a/app/assets/javascripts/ide/ide_router.js +++ b/app/assets/javascripts/ide/ide_router.js @@ -3,7 +3,6 @@ import VueRouter from 'vue-router'; import { join as joinPath } from 'path'; import flash from '~/flash'; import store from './stores'; -import { activityBarViews } from './constants'; Vue.use(VueRouter); @@ -74,98 +73,23 @@ router.beforeEach((to, from, next) => { projectId: to.params.project, }) .then(() => { - const fullProjectId = `${to.params.namespace}/${to.params.project}`; - + const basePath = to.params[0] || ''; + const projectId = `${to.params.namespace}/${to.params.project}`; const branchId = to.params.branchid; + const mergeRequestId = to.params.mrid; if (branchId) { - const basePath = to.params[0] || ''; - - store.dispatch('setCurrentBranchId', branchId); - - store.dispatch('getBranchData', { - projectId: fullProjectId, + store.dispatch('openBranch', { + projectId, branchId, + basePath, + }); + } else if (mergeRequestId) { + store.dispatch('openMergeRequest', { + projectId, + mergeRequestId, + targetProjectId: to.query.target_project, }); - - store - .dispatch('getFiles', { - projectId: fullProjectId, - branchId, - }) - .then(() => { - if (basePath) { - const path = basePath.slice(-1) === '/' ? basePath.slice(0, -1) : basePath; - const treeEntryKey = Object.keys(store.state.entries).find( - key => key === path && !store.state.entries[key].pending, - ); - const treeEntry = store.state.entries[treeEntryKey]; - - if (treeEntry) { - store.dispatch('handleTreeEntryAction', treeEntry); - } - } - }) - .catch(e => { - throw e; - }); - } else if (to.params.mrid) { - store - .dispatch('getMergeRequestData', { - projectId: fullProjectId, - targetProjectId: to.query.target_project, - mergeRequestId: to.params.mrid, - }) - .then(mr => { - store.dispatch('updateActivityBarView', activityBarViews.review); - - store.dispatch('getBranchData', { - projectId: fullProjectId, - branchId: mr.source_branch, - }); - - return store.dispatch('getFiles', { - projectId: fullProjectId, - branchId: mr.source_branch, - }); - }) - .then(() => - store.dispatch('getMergeRequestVersions', { - projectId: fullProjectId, - targetProjectId: to.query.target_project, - mergeRequestId: to.params.mrid, - }), - ) - .then(() => - store.dispatch('getMergeRequestChanges', { - projectId: fullProjectId, - targetProjectId: to.query.target_project, - mergeRequestId: to.params.mrid, - }), - ) - .then(mrChanges => { - mrChanges.changes.forEach((change, ind) => { - const changeTreeEntry = store.state.entries[change.new_path]; - - if (changeTreeEntry) { - store.dispatch('setFileMrChange', { - file: changeTreeEntry, - mrChange: change, - }); - - if (ind < 10) { - store.dispatch('getFileData', { - path: change.new_path, - makeFileActive: ind === 0, - }); - } - } - }); - }) - .catch(e => { - flash('Error while loading the merge request. Please try again.'); - throw e; - }); } }) .catch(e => { diff --git a/app/assets/javascripts/ide/stores/actions/file.js b/app/assets/javascripts/ide/stores/actions/file.js index 9e3f5da4676..28b9d0df201 100644 --- a/app/assets/javascripts/ide/stores/actions/file.js +++ b/app/assets/javascripts/ide/stores/actions/file.js @@ -54,9 +54,6 @@ export const setFileActive = ({ commit, state, getters, dispatch }, path) => { commit(types.SET_FILE_ACTIVE, { path, active: true }); dispatch('scrollToTab'); - - commit(types.SET_CURRENT_PROJECT, file.projectId); - commit(types.SET_CURRENT_BRANCH, file.branchId); }; export const getFileData = ({ state, commit, dispatch }, { path, makeFileActive = true }) => { @@ -95,7 +92,7 @@ export const setFileMrChange = ({ commit }, { file, mrChange }) => { commit(types.SET_FILE_MERGE_REQUEST_CHANGE, { file, mrChange }); }; -export const getRawFileData = ({ state, commit, dispatch }, { path, baseSha }) => { +export const getRawFileData = ({ state, commit, dispatch, getters }, { path }) => { const file = state.entries[path]; return new Promise((resolve, reject) => { service @@ -103,6 +100,9 @@ export const getRawFileData = ({ state, commit, dispatch }, { path, baseSha }) = .then(raw => { if (!(file.tempFile && !file.prevPath)) commit(types.SET_FILE_RAW_DATA, { file, raw }); if (file.mrChange && file.mrChange.new_file === false) { + const baseSha = + (getters.currentMergeRequest && getters.currentMergeRequest.baseCommitSha) || ''; + service .getBaseRawFileData(file, baseSha) .then(baseRaw => { @@ -125,7 +125,7 @@ export const getRawFileData = ({ state, commit, dispatch }, { path, baseSha }) = action: payload => dispatch('getRawFileData', payload).then(() => dispatch('setErrorMessage', null)), actionText: __('Please try again'), - actionPayload: { path, baseSha }, + actionPayload: { path }, }); reject(); }); diff --git a/app/assets/javascripts/ide/stores/actions/merge_request.js b/app/assets/javascripts/ide/stores/actions/merge_request.js index 1887b77b00b..187f8c75d07 100644 --- a/app/assets/javascripts/ide/stores/actions/merge_request.js +++ b/app/assets/javascripts/ide/stores/actions/merge_request.js @@ -1,6 +1,8 @@ -import { __ } from '../../../locale'; +import flash from '~/flash'; +import { __ } from '~/locale'; import service from '../../services'; import * as types from '../mutation_types'; +import { activityBarViews } from '../../constants'; export const getMergeRequestData = ( { commit, dispatch, state }, @@ -104,3 +106,67 @@ export const getMergeRequestVersions = ( resolve(state.projects[projectId].mergeRequests[mergeRequestId].versions); } }); + +export const openMergeRequest = ( + { dispatch, state }, + { projectId, targetProjectId, mergeRequestId } = {}, +) => + dispatch('getMergeRequestData', { + projectId, + targetProjectId, + mergeRequestId, + }) + .then(mr => { + dispatch('setCurrentBranchId', mr.source_branch); + + dispatch('getBranchData', { + projectId, + branchId: mr.source_branch, + }); + + return dispatch('getFiles', { + projectId, + branchId: mr.source_branch, + }); + }) + .then(() => + dispatch('getMergeRequestVersions', { + projectId, + targetProjectId, + mergeRequestId, + }), + ) + .then(() => + dispatch('getMergeRequestChanges', { + projectId, + targetProjectId, + mergeRequestId, + }), + ) + .then(mrChanges => { + if (mrChanges.changes.length) { + dispatch('updateActivityBarView', activityBarViews.review); + } + + mrChanges.changes.forEach((change, ind) => { + const changeTreeEntry = state.entries[change.new_path]; + + if (changeTreeEntry) { + dispatch('setFileMrChange', { + file: changeTreeEntry, + mrChange: change, + }); + + if (ind < 10) { + dispatch('getFileData', { + path: change.new_path, + makeFileActive: ind === 0, + }); + } + } + }); + }) + .catch(e => { + flash(__('Error while loading the merge request. Please try again.')); + throw e; + }); diff --git a/app/assets/javascripts/ide/stores/actions/project.js b/app/assets/javascripts/ide/stores/actions/project.js index 501e25d452b..543dc6c0461 100644 --- a/app/assets/javascripts/ide/stores/actions/project.js +++ b/app/assets/javascripts/ide/stores/actions/project.js @@ -124,3 +124,35 @@ export const showBranchNotFoundError = ({ dispatch }, branchId) => { actionPayload: branchId, }); }; + +export const openBranch = ( + { dispatch, state }, + { projectId, branchId, basePath }, +) => { + dispatch('setCurrentBranchId', branchId); + + dispatch('getBranchData', { + projectId, + branchId, + }); + + return ( + dispatch('getFiles', { + projectId, + branchId, + }) + .then(() => { + if (basePath) { + const path = basePath.slice(-1) === '/' ? basePath.slice(0, -1) : basePath; + const treeEntryKey = Object.keys(state.entries).find( + key => key === path && !state.entries[key].pending, + ); + const treeEntry = state.entries[treeEntryKey]; + + if (treeEntry) { + dispatch('handleTreeEntryAction', treeEntry); + } + } + }) + ); +}; diff --git a/app/assets/javascripts/ide/stores/modules/branches/actions.js b/app/assets/javascripts/ide/stores/modules/branches/actions.js index 74aa98ef9f9..f90c2d77f2b 100644 --- a/app/assets/javascripts/ide/stores/modules/branches/actions.js +++ b/app/assets/javascripts/ide/stores/modules/branches/actions.js @@ -33,7 +33,4 @@ export const fetchBranches = ({ dispatch, rootGetters }, { search = '' }) => { export const resetBranches = ({ commit }) => commit(types.RESET_BRANCHES); -export const openBranch = ({ rootState, dispatch }, id) => - dispatch('goToRoute', `/project/${rootState.currentProjectId}/edit/${id}`, { root: true }); - export default () => {}; diff --git a/app/assets/javascripts/ide/stores/mutations.js b/app/assets/javascripts/ide/stores/mutations.js index 1eda5768709..56a8d9430c7 100644 --- a/app/assets/javascripts/ide/stores/mutations.js +++ b/app/assets/javascripts/ide/stores/mutations.js @@ -200,6 +200,7 @@ export default { }, [types.DELETE_ENTRY](state, path) { const entry = state.entries[path]; + const { tempFile = false } = entry; const parent = entry.parentPath ? state.entries[entry.parentPath] : state.trees[`${state.currentProjectId}/${state.currentBranchId}`]; @@ -209,7 +210,11 @@ export default { parent.tree = parent.tree.filter(f => f.path !== entry.path); if (entry.type === 'blob') { - state.changedFiles = state.changedFiles.concat(entry); + if (tempFile) { + state.changedFiles = state.changedFiles.filter(f => f.path !== path); + } else { + state.changedFiles = state.changedFiles.concat(entry); + } } }, [types.RENAME_ENTRY](state, { path, name, entryPath = null }) { diff --git a/app/assets/javascripts/importer_status.js b/app/assets/javascripts/importer_status.js index 0035d809062..eda8cdad908 100644 --- a/app/assets/javascripts/importer_status.js +++ b/app/assets/javascripts/importer_status.js @@ -87,7 +87,7 @@ class ImporterStatus { details = error.response.data.errors; } - flash(__(`An error occurred while importing project: ${details}`)); + flash(sprintf(__('An error occurred while importing project: %{details}'), { details })); }); } diff --git a/app/assets/javascripts/jobs/components/artifacts_block.vue b/app/assets/javascripts/jobs/components/artifacts_block.vue new file mode 100644 index 00000000000..525c5eec91a --- /dev/null +++ b/app/assets/javascripts/jobs/components/artifacts_block.vue @@ -0,0 +1,98 @@ +<script> + import TimeagoTooltiop from '~/vue_shared/components/time_ago_tooltip.vue'; + + export default { + components: { + TimeagoTooltiop, + }, + props: { + // @build.artifacts_expired? + haveArtifactsExpired: { + type: Boolean, + required: true, + }, + // @build.has_expiring_artifacts? + willArtifactsExpire: { + type: Boolean, + required: true, + }, + expireAt: { + type: String, + required: false, + default: null, + }, + keepArtifactsPath: { + type: String, + required: false, + default: null, + }, + downloadArtifactsPath: { + type: String, + required: false, + default: null, + }, + browseArtifactsPath: { + type: String, + required: false, + default: null, + }, + }, + }; +</script> +<template> + <div class="block"> + <div class="title"> + {{ s__('Job|Job artifacts') }} + </div> + + <p + v-if="haveArtifactsExpired" + class="js-artifacts-removed build-detail-row" + > + {{ s__('Job|The artifacts were removed') }} + </p> + <p + v-else-if="willArtifactsExpire" + class="js-artifacts-will-be-removed build-detail-row" + > + {{ s__('Job|The artifacts will be removed') }} + </p> + + <timeago-tooltiop + v-if="expireAt" + :time="expireAt" + /> + + <div + class="btn-group d-flex" + role="group" + > + <a + v-if="keepArtifactsPath" + :href="keepArtifactsPath" + class="js-keep-artifacts btn btn-sm btn-default" + data-method="post" + > + {{ s__('Job|Keep') }} + </a> + + <a + v-if="downloadArtifactsPath" + :href="downloadArtifactsPath" + class="js-download-artifacts btn btn-sm btn-default" + download + rel="nofollow" + > + {{ s__('Job|Download') }} + </a> + + <a + v-if="browseArtifactsPath" + :href="browseArtifactsPath" + class="js-browse-artifacts btn btn-sm btn-default" + > + {{ s__('Job|Browse') }} + </a> + </div> + </div> +</template> diff --git a/app/assets/javascripts/jobs/components/commit_block.vue b/app/assets/javascripts/jobs/components/commit_block.vue new file mode 100644 index 00000000000..7f485295513 --- /dev/null +++ b/app/assets/javascripts/jobs/components/commit_block.vue @@ -0,0 +1,64 @@ +<script> +import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; + +export default { + components: { + ClipboardButton, + }, + props: { + pipelineShortSha: { + type: String, + required: true, + }, + pipelineShaPath: { + type: String, + required: true, + }, + mergeRequestReference: { + type: String, + required: false, + default: null, + }, + mergeRequestPath: { + type: String, + required: false, + default: null, + }, + gitCommitTitlte: { + type: String, + required: true, + }, + }, +}; +</script> +<template> + <div class="block"> + <p> + {{ __('Commit') }} + + <a + :href="pipelineShaPath" + class="js-commit-sha commit-sha link-commit" + > + {{ pipelineShortSha }} + </a> + + <clipboard-button + :text="pipelineShortSha" + :title="__('Copy commit SHA to clipboard')" + /> + + <a + v-if="mergeRequestPath && mergeRequestReference" + :href="mergeRequestPath" + class="js-link-commit link-commit" + > + {{ mergeRequestReference }} + </a> + </p> + + <p class="build-light-text append-bottom-0"> + {{ gitCommitTitlte }} + </p> + </div> +</template> diff --git a/app/assets/javascripts/jobs/components/empty_state.vue b/app/assets/javascripts/jobs/components/empty_state.vue new file mode 100644 index 00000000000..4faf08387fb --- /dev/null +++ b/app/assets/javascripts/jobs/components/empty_state.vue @@ -0,0 +1,76 @@ +<script> + export default { + props: { + illustrationPath: { + type: String, + required: true, + }, + illustrationSizeClass: { + type: String, + required: true, + }, + title: { + type: String, + required: true, + }, + content: { + type: String, + required: false, + default: null, + }, + action: { + type: Object, + required: false, + default: null, + validator(value) { + return ( + value === null || + (Object.prototype.hasOwnProperty.call(value, 'link') && + Object.prototype.hasOwnProperty.call(value, 'method') && + Object.prototype.hasOwnProperty.call(value, 'title')) + ); + }, + }, + }, + }; +</script> +<template> + <div class="row empty-state"> + <div class="col-12"> + <div + :class="illustrationSizeClass" + class="svg-content" + > + <img :src="illustrationPath" /> + </div> + </div> + + <div class="col-12"> + <div class="text-content"> + <h4 class="js-job-empty-state-title text-center"> + {{ title }} + </h4> + + <p + v-if="content" + class="js-job-empty-state-content" + > + {{ content }} + </p> + + <div + v-if="action" + class="text-center" + > + <a + :href="action.link" + :data-method="action.method" + class="js-job-empty-state-action btn btn-primary" + > + {{ action.title }} + </a> + </div> + </div> + </div> + </div> +</template> diff --git a/app/assets/javascripts/jobs/components/erased_block.vue b/app/assets/javascripts/jobs/components/erased_block.vue new file mode 100644 index 00000000000..d688eebfa95 --- /dev/null +++ b/app/assets/javascripts/jobs/components/erased_block.vue @@ -0,0 +1,48 @@ +<script> +import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; + +export default { + components: { + TimeagoTooltip, + }, + props: { + erasedByUser: { + type: Boolean, + required: true, + }, + username: { + type: String, + required: false, + default: null, + }, + linkToUser: { + type: String, + required: false, + default: null, + }, + erasedAt: { + type: String, + required: true, + }, + }, +}; +</script> +<template> + <div class="prepend-top-default js-build-erased"> + <div class="erased alert alert-warning"> + <template v-if="erasedByUser"> + {{ s__("Job|Job has been erased by") }} + <a :href="linkToUser"> + {{ username }} + </a> + </template> + <template v-else> + {{ s__("Job|Job has been erased") }} + </template> + + <timeago-tooltip + :time="erasedAt" + /> + </div> + </div> +</template> diff --git a/app/assets/javascripts/jobs/components/job_log.vue b/app/assets/javascripts/jobs/components/job_log.vue new file mode 100644 index 00000000000..3c4749d996b --- /dev/null +++ b/app/assets/javascripts/jobs/components/job_log.vue @@ -0,0 +1,33 @@ +<script> + export default { + name: 'JobLog', + props: { + trace: { + type: String, + required: true, + }, + isReceivingBuildTrace: { + type: Boolean, + required: true, + }, + }, + }; +</script> +<template> + <pre class="build-trace"> + <code + class="bash" + v-html="trace" + > + </code> + + <div + v-if="isReceivingBuildTrace" + class="js-log-animation build-loader-animation" + > + <div class="dot"></div> + <div class="dot"></div> + <div class="dot"></div> + </div> + </pre> +</template> diff --git a/app/assets/javascripts/jobs/components/job_log_controllers.vue b/app/assets/javascripts/jobs/components/job_log_controllers.vue new file mode 100644 index 00000000000..513851e376f --- /dev/null +++ b/app/assets/javascripts/jobs/components/job_log_controllers.vue @@ -0,0 +1,139 @@ +<script> + import Icon from '~/vue_shared/components/icon.vue'; + import tooltip from '~/vue_shared/directives/tooltip'; + import { numberToHumanSize } from '~/lib/utils/number_utils'; + import { s__, sprintf } from '~/locale'; + + export default { + components: { + Icon, + }, + directives: { + tooltip, + }, + props: { + canEraseJob: { + type: Boolean, + required: true, + }, + size: { + type: Number, + required: true, + }, + rawTracePath: { + type: String, + required: false, + default: null, + }, + canScrollToTop: { + type: Boolean, + required: true, + }, + canScrollToBottom: { + type: Boolean, + required: true, + }, + }, + computed: { + jobLogSize() { + return sprintf('Showing last %{startSpanTag} %{size} %{endSpanTag} of log -', { + startSpanTag: '<span class="s-truncated-info-size truncated-info-size">', + endSpanTag: '</span>', + size: numberToHumanSize(this.size), + }); + }, + }, + methods: { + handleEraseJobClick() { + // eslint-disable-next-line no-alert + if (window.confirm(s__('Job|Are you sure you want to erase this job?'))) { + this.$emit('eraseJob'); + } + }, + handleScrollToTop() { + this.$emit('scrollJobLogTop'); + }, + handleScrollToBottom() { + this.$emit('scrollJobLogBottom'); + }, + }, + }; +</script> +<template> + <div class="top-bar"> + <!-- truncate information --> + <div class="js-truncated-info truncated-info d-none d-sm-block float-left"> + <p v-html="jobLogSize"></p> + + <a + v-if="rawTracePath" + :href="rawTracePath" + class="js-raw-link raw-link" + > + {{ s__("Job|Complete Raw") }} + </a> + </div> + <!-- eo truncate information --> + + <div class="controllers float-right"> + <!-- links --> + <a + v-tooltip + v-if="rawTracePath" + :title="s__('Job|Show complete raw')" + :href="rawTracePath" + class="js-raw-link-controller controllers-buttons" + data-container="body" + > + <icon name="doc-text" /> + </a> + + <button + v-tooltip + v-if="canEraseJob" + :title="s__('Job|Erase job log')" + type="button" + class="js-erase-link controllers-buttons" + data-container="body" + @click="handleEraseJobClick" + > + <icon name="remove" /> + </button> + <!-- eo links --> + + <!-- scroll buttons --> + <div + v-tooltip + :title="s__('Job|Scroll to top')" + class="controllers-buttons" + data-container="body" + > + <button + :disabled="!canScrollToTop" + type="button" + class="js-scroll-top btn-scroll btn-transparent btn-blank" + @click="handleScrollToTop" + > + <icon name="scroll_up"/> + </button> + </div> + + <div + v-tooltip + :title="s__('Job|Scroll to bottom')" + class="controllers-buttons" + data-container="body" + > + <button + :disabled="!canScrollToBottom" + type="button" + class="js-scroll-bottom btn-scroll btn-transparent btn-blank" + @click="handleScrollToBottom" + > + <icon name="scroll_down"/> + </button> + </div> + <!-- eo scroll buttons --> + </div> + </div> +</template> diff --git a/app/assets/javascripts/jobs/components/jobs_container.vue b/app/assets/javascripts/jobs/components/jobs_container.vue new file mode 100644 index 00000000000..b81109bdd06 --- /dev/null +++ b/app/assets/javascripts/jobs/components/jobs_container.vue @@ -0,0 +1,60 @@ +<script> + import CiIcon from '~/vue_shared/components/ci_icon.vue'; + import Icon from '~/vue_shared/components/icon.vue'; + import tooltip from '~/vue_shared/directives/tooltip'; + + export default { + components: { + CiIcon, + Icon, + }, + directives: { + tooltip, + }, + props: { + jobs: { + type: Array, + required: true, + }, + }, + }; +</script> +<template> + <div class="builds-container"> + <div + class="build-job" + > + <a + v-tooltip + v-for="job in jobs" + :key="job.id" + :href="job.path" + :title="job.tooltip" + :class="{ active: job.active, retried: job.retried }" + > + <icon + v-if="job.active" + name="arrow-right" + class="js-arrow-right" + /> + + <ci-icon :status="job.status" /> + + <span> + <template v-if="job.name"> + {{ job.name }} + </template> + <template v-else> + {{ job.id }} + </template> + </span> + + <icon + v-if="job.retried" + name="retry" + class="js-retry-icon" + /> + </a> + </div> + </div> +</template> diff --git a/app/assets/javascripts/jobs/components/sidebar_details_block.vue b/app/assets/javascripts/jobs/components/sidebar_details_block.vue index d2adf628050..36d4a3e2bc9 100644 --- a/app/assets/javascripts/jobs/components/sidebar_details_block.vue +++ b/app/assets/javascripts/jobs/components/sidebar_details_block.vue @@ -1,14 +1,16 @@ <script> -import detailRow from './sidebar_detail_row.vue'; -import loadingIcon from '../../vue_shared/components/loading_icon.vue'; -import timeagoMixin from '../../vue_shared/mixins/timeago'; -import { timeIntervalInWords } from '../../lib/utils/datetime_utility'; +import LoadingIcon from '~/vue_shared/components/loading_icon.vue'; +import timeagoMixin from '~/vue_shared/mixins/timeago'; +import { timeIntervalInWords } from '~/lib/utils/datetime_utility'; +import Icon from '~/vue_shared/components/icon.vue'; +import DetailRow from './sidebar_detail_row.vue'; export default { name: 'SidebarDetailsBlock', components: { - detailRow, - loadingIcon, + DetailRow, + LoadingIcon, + Icon, }, mixins: [timeagoMixin], props: { @@ -20,16 +22,16 @@ export default { type: Boolean, required: true, }, - canUserRetry: { - type: Boolean, - required: false, - default: false, - }, runnerHelpUrl: { type: String, required: false, default: '', }, + terminalPath: { + type: String, + required: false, + default: null, + }, }, computed: { shouldRenderContent() { @@ -92,7 +94,7 @@ export default { {{ job.name }} </strong> <a - v-if="canUserRetry" + v-if="job.retry_path" :class="retryButtonClass" :href="job.retry_path" data-method="post" @@ -100,6 +102,16 @@ export default { > {{ __('Retry') }} </a> + <a + v-if="terminalPath" + :href="terminalPath" + class="js-terminal-link pull-right btn btn-primary + btn-inverted visible-md-block visible-lg-block" + target="_blank" + > + {{ __('Debug') }} + <icon name="external-link" /> + </a> <button :aria-label="__('Toggle Sidebar')" type="button" @@ -125,7 +137,7 @@ export default { {{ __('New issue') }} </a> <a - v-if="canUserRetry" + v-if="job.retry_path" :href="job.retry_path" class="js-retry-job btn btn-inverted-secondary" data-method="post" diff --git a/app/assets/javascripts/jobs/components/stages_dropdown.vue b/app/assets/javascripts/jobs/components/stages_dropdown.vue new file mode 100644 index 00000000000..d6d64fa32f7 --- /dev/null +++ b/app/assets/javascripts/jobs/components/stages_dropdown.vue @@ -0,0 +1,97 @@ +<script> + import CiIcon from '~/vue_shared/components/ci_icon.vue'; + import Icon from '~/vue_shared/components/icon.vue'; + + import { sprintf, __ } from '~/locale'; + + export default { + components: { + CiIcon, + Icon, + }, + props: { + pipelineId: { + type: Number, + required: true, + }, + pipelinePath: { + type: String, + required: true, + }, + pipelineRef: { + type: String, + required: true, + }, + pipelineRefPath: { + type: String, + required: true, + }, + stages: { + type: Array, + required: true, + }, + pipelineStatus: { + type: Object, + required: true, + }, + }, + data() { + return { + selectedStage: this.stages.length > 0 ? this.stages[0].name : __('More'), + }; + }, + computed: { + pipelineLink() { + return sprintf(__('Pipeline %{pipelineLinkStart} #%{pipelineId} %{pipelineLinkEnd} from %{pipelineLinkRefStart} %{pipelineRef} %{pipelineLinkRefEnd}'), { + pipelineLinkStart: `<a href=${this.pipelinePath} class="js-pipeline-path link-commit">`, + pipelineId: this.pipelineId, + pipelineLinkEnd: '</a>', + pipelineLinkRefStart: `<a href=${this.pipelineRefPath} class="link-commit ref-name">`, + pipelineRef: this.pipelineRef, + pipelineLinkRefEnd: '</a>', + }, false); + }, + }, + methods: { + onStageClick(stage) { + // todo: consider moving into store + this.selectedStage = stage.name; + + // update dropdown with jobs + // jobs container is a new component. + this.$emit('requestSidebarStageDropdown', stage); + }, + }, + }; +</script> +<template> + <div class="block-last"> + <ci-icon :status="pipelineStatus" /> + + <p v-html="pipelineLink"></p> + + <div class="dropdown"> + <button + type="button" + data-toggle="dropdown" + > + {{ selectedStage }} + <icon name="chevron-down" /> + </button> + <ul class="dropdown-menu"> + <li + v-for="(stage, index) in stages" + :key="index" + > + <button + type="button" + class="stage-item" + @click="onStageClick(stage)" + > + {{ stage.name }} + </button> + </li> + </ul> + </div> + </div> +</template> diff --git a/app/assets/javascripts/jobs/components/stuck_block.vue b/app/assets/javascripts/jobs/components/stuck_block.vue new file mode 100644 index 00000000000..18883fea950 --- /dev/null +++ b/app/assets/javascripts/jobs/components/stuck_block.vue @@ -0,0 +1,63 @@ +<script> +/** + * Renders Stuck Runners block for job's view. + */ +export default { + props: { + hasNoRunnersForProject: { + type: Boolean, + required: true, + }, + tags: { + type: Array, + required: false, + default: () => [], + }, + runnersPath: { + type: String, + required: true, + }, + }, +}; +</script> +<template> + <div class="bs-callout bs-callout-warning"> + <p + v-if="hasNoRunnersForProject" + class="js-stuck-no-runners" + > + {{ s__(`Job|This job is stuck, because the project + doesn't have any runners online assigned to it.`) }} + </p> + <p + v-else-if="tags.length" + class="js-stuck-with-tags" + > + {{ s__(`This job is stuck, because you don't have + any active runners online with any of these tags assigned to them:`) }} + <span + v-for="(tag, index) in tags" + :key="index" + class="badge badge-primary" + > + {{ tag }} + </span> + </p> + <p + v-else + class="js-stuck-no-active-runner" + > + {{ s__(`This job is stuck, because you don't + have any active runners that can run this job.`) }} + </p> + + {{ __("Go to") }} + <a + v-if="runnersPath" + :href="runnersPath" + class="js-runners-path" + > + {{ __("Runners page") }} + </a> + </div> +</template> diff --git a/app/assets/javascripts/jobs/components/trigger_block.vue b/app/assets/javascripts/jobs/components/trigger_block.vue new file mode 100644 index 00000000000..8a88e5da6aa --- /dev/null +++ b/app/assets/javascripts/jobs/components/trigger_block.vue @@ -0,0 +1,84 @@ +<script> + export default { + props: { + shortToken: { + type: String, + required: false, + default: null, + }, + + variables: { + type: Object, + required: false, + default: () => ({}), + }, + }, + data() { + return { + areVariablesVisible: false, + }; + }, + computed: { + hasVariables() { + return Object.keys(this.variables).length > 0; + }, + }, + methods: { + revealVariables() { + this.areVariablesVisible = true; + }, + }, + }; +</script> + +<template> + <div class="build-widget block"> + <h4 class="title"> + {{ __('Trigger') }} + </h4> + + <p + v-if="shortToken" + class="js-short-token" + > + <span class="build-light-text"> + {{ __('Token') }} + </span> + {{ shortToken }} + </p> + + <p v-if="hasVariables"> + <button + type="button" + class="btn btn-default group js-reveal-variables" + @click="revealVariables" + > + {{ __('Reveal Variables') }} + </button> + + </p> + + <dl + v-if="areVariablesVisible" + class="js-build-variables trigger-build-variables" + > + <template + v-for="(value, key) in variables" + > + <dt + :key="`${key}-variable`" + class="js-build-variable trigger-build-variable" + > + {{ key }} + </dt> + + <dd + :key="`${key}-value`" + class="js-build-value trigger-build-value" + > + {{ value }} + </dd> + </template> + </dl> + </div> +</template> diff --git a/app/assets/javascripts/jobs/job_details_bundle.js b/app/assets/javascripts/jobs/job_details_bundle.js index 0db7b95636c..a84324f14b2 100644 --- a/app/assets/javascripts/jobs/job_details_bundle.js +++ b/app/assets/javascripts/jobs/job_details_bundle.js @@ -52,9 +52,9 @@ export default () => { return createElement('details-block', { props: { isLoading: this.mediator.state.isLoading, - canUserRetry: !!('canUserRetry' in detailsBlockDataset), job: this.mediator.store.state.job, runnerHelpUrl: dataset.runnerHelpUrl, + terminalPath: detailsBlockDataset.terminalPath, }, }); }, diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js index cb851ff6745..6499b919787 100644 --- a/app/assets/javascripts/labels_select.js +++ b/app/assets/javascripts/labels_select.js @@ -4,7 +4,7 @@ import $ from 'jquery'; import _ from 'underscore'; -import { __ } from './locale'; +import { sprintf, __ } from './locale'; import axios from './lib/utils/axios_utils'; import IssuableBulkUpdateActions from './issuable_bulk_update_actions'; import DropdownUtils from './filtered_search/dropdown_utils'; @@ -39,7 +39,7 @@ export default class LabelsSelect { showNo = $dropdown.data('showNo'); showAny = $dropdown.data('showAny'); showMenuAbove = $dropdown.data('showMenuAbove'); - defaultLabel = $dropdown.data('defaultLabel') || 'Label'; + defaultLabel = $dropdown.data('defaultLabel') || __('Label'); abilityName = $dropdown.data('abilityName'); $selectbox = $dropdown.closest('.selectbox'); $block = $selectbox.closest('.block'); @@ -267,7 +267,10 @@ export default class LabelsSelect { return selectedLabels; } else if (selectedLabels.length) { - return selectedLabels[0] + " +" + (selectedLabels.length - 1) + " more"; + return sprintf(__('%{firstLabel} +%{labelCount} more'), { + firstLabel: selectedLabels[0], + labelCount: selectedLabels.length - 1 + }); } else { return defaultLabel; diff --git a/app/assets/javascripts/locale/ensure_single_line.js b/app/assets/javascripts/locale/ensure_single_line.js new file mode 100644 index 00000000000..47c52fe6c50 --- /dev/null +++ b/app/assets/javascripts/locale/ensure_single_line.js @@ -0,0 +1,25 @@ +/* eslint-disable import/no-commonjs */ + +const SPLIT_REGEX = /\s*[\r\n]+\s*/; + +/** + * + * strips newlines from strings and replaces them with a single space + * + * @example + * + * ensureSingleLine('foo \n bar') === 'foo bar' + * + * @param {String} str + * @returns {String} + */ +module.exports = function ensureSingleLine(str) { + // This guard makes the function significantly faster + if (str.includes('\n') || str.includes('\r')) { + return str + .split(SPLIT_REGEX) + .filter(s => s !== '') + .join(' '); + } + return str; +}; diff --git a/app/assets/javascripts/locale/index.js b/app/assets/javascripts/locale/index.js index 2cc5fb10027..1ae3362c4bc 100644 --- a/app/assets/javascripts/locale/index.js +++ b/app/assets/javascripts/locale/index.js @@ -1,4 +1,5 @@ import Jed from 'jed'; +import ensureSingleLine from './ensure_single_line'; import sprintf from './sprintf'; const languageCode = () => document.querySelector('html').getAttribute('lang') || 'en'; @@ -10,7 +11,7 @@ delete window.translations; @param text The text to be translated @returns {String} The translated text */ -const gettext = locale.gettext.bind(locale); +const gettext = text => locale.gettext.bind(locale)(ensureSingleLine(text)); /** Translate the text with a number @@ -23,7 +24,10 @@ const gettext = locale.gettext.bind(locale); @returns {String} Translated text with the number replaced (eg. '2 days') */ const ngettext = (text, pluralText, count) => { - const translated = locale.ngettext(text, pluralText, count).replace(/%d/g, count).split('|'); + const translated = locale + .ngettext(ensureSingleLine(text), ensureSingleLine(pluralText), count) + .replace(/%d/g, count) + .split('|'); return translated[translated.length - 1]; }; @@ -40,7 +44,7 @@ const ngettext = (text, pluralText, count) => { @returns {String} Translated context based text */ const pgettext = (keyOrContext, key) => { - const normalizedKey = key ? `${keyOrContext}|${key}` : keyOrContext; + const normalizedKey = ensureSingleLine(key ? `${keyOrContext}|${key}` : keyOrContext); const translated = gettext(normalizedKey).split('|'); return translated[translated.length - 1]; @@ -52,8 +56,7 @@ const pgettext = (keyOrContext, key) => { @param formatOptions for available options, please see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DateTimeFormat @returns {Intl.DateTimeFormat} */ -const createDateTimeFormat = - formatOptions => Intl.DateTimeFormat(languageCode(), formatOptions); +const createDateTimeFormat = formatOptions => Intl.DateTimeFormat(languageCode(), formatOptions); export { languageCode }; export { gettext as __ }; diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue index 6afaefc56f8..ae96ac3b80c 100644 --- a/app/assets/javascripts/monitoring/components/dashboard.vue +++ b/app/assets/javascripts/monitoring/components/dashboard.vue @@ -172,7 +172,7 @@ export default { <template> <div v-if="!showEmptyState" - class="prometheus-graphs prepend-top-10" + class="prometheus-graphs prepend-top-default" > <div class="environments d-flex align-items-center"> {{ s__('Metrics|Environment') }} diff --git a/app/assets/javascripts/pages/admin/application_settings/index.js b/app/assets/javascripts/pages/admin/application_settings/index.js index 48d75f5443b..47bd70537f1 100644 --- a/app/assets/javascripts/pages/admin/application_settings/index.js +++ b/app/assets/javascripts/pages/admin/application_settings/index.js @@ -1,6 +1,8 @@ import initSettingsPanels from '~/settings_panels'; +import projectSelect from '~/project_select'; document.addEventListener('DOMContentLoaded', () => { // Initialize expandable settings panels initSettingsPanels(); + projectSelect(); }); diff --git a/app/assets/javascripts/pages/admin/cohorts/index.js b/app/assets/javascripts/pages/instance_statistics/cohorts/index.js index 2d5020dbef4..2d5020dbef4 100644 --- a/app/assets/javascripts/pages/admin/cohorts/index.js +++ b/app/assets/javascripts/pages/instance_statistics/cohorts/index.js diff --git a/app/assets/javascripts/pages/admin/cohorts/usage_ping.js b/app/assets/javascripts/pages/instance_statistics/cohorts/usage_ping.js index 914a9661c27..914a9661c27 100644 --- a/app/assets/javascripts/pages/admin/cohorts/usage_ping.js +++ b/app/assets/javascripts/pages/instance_statistics/cohorts/usage_ping.js diff --git a/app/assets/javascripts/pages/admin/conversational_development_index/show/index.js b/app/assets/javascripts/pages/instance_statistics/conversational_development_index/index.js index c1056537f90..c1056537f90 100644 --- a/app/assets/javascripts/pages/admin/conversational_development_index/show/index.js +++ b/app/assets/javascripts/pages/instance_statistics/conversational_development_index/index.js diff --git a/app/assets/javascripts/pages/projects/settings/repository/show/index.js b/app/assets/javascripts/pages/projects/settings/repository/show/index.js index ffc84dc106b..78cf5406e43 100644 --- a/app/assets/javascripts/pages/projects/settings/repository/show/index.js +++ b/app/assets/javascripts/pages/projects/settings/repository/show/index.js @@ -1,3 +1,9 @@ import initForm from '../form'; +import MirrorRepos from './mirror_repos'; -document.addEventListener('DOMContentLoaded', initForm); +document.addEventListener('DOMContentLoaded', () => { + initForm(); + + const mirrorReposContainer = document.querySelector('.js-mirror-settings'); + if (mirrorReposContainer) new MirrorRepos(mirrorReposContainer).init(); +}); diff --git a/app/assets/javascripts/pages/projects/settings/repository/show/mirror_repos.js b/app/assets/javascripts/pages/projects/settings/repository/show/mirror_repos.js new file mode 100644 index 00000000000..4c56af20cc3 --- /dev/null +++ b/app/assets/javascripts/pages/projects/settings/repository/show/mirror_repos.js @@ -0,0 +1,94 @@ +import $ from 'jquery'; +import _ from 'underscore'; +import { __ } from '~/locale'; +import Flash from '~/flash'; +import axios from '~/lib/utils/axios_utils'; + +export default class MirrorRepos { + constructor(container) { + this.$container = $(container); + this.$form = $('.js-mirror-form', this.$container); + this.$urlInput = $('.js-mirror-url', this.$form); + this.$protectedBranchesInput = $('.js-mirror-protected', this.$form); + this.$table = $('.js-mirrors-table-body', this.$container); + this.mirrorEndpoint = this.$form.data('projectMirrorEndpoint'); + } + + init() { + this.initMirrorPush(); + this.registerUpdateListeners(); + } + + initMirrorPush() { + this.$passwordGroup = $('.js-password-group', this.$container); + this.$password = $('.js-password', this.$passwordGroup); + this.$authMethod = $('.js-auth-method', this.$form); + + this.$authMethod.on('change', () => this.togglePassword()); + this.$password.on('input.updateUrl', () => this.debouncedUpdateUrl()); + } + + updateUrl() { + let val = this.$urlInput.val(); + + if (this.$password) { + const password = this.$password.val(); + if (password) val = val.replace('@', `:${password}@`); + } + + $('.js-mirror-url-hidden', this.$form).val(val); + } + + updateProtectedBranches() { + const val = this.$protectedBranchesInput.get(0).checked + ? this.$protectedBranchesInput.val() + : '0'; + $('.js-mirror-protected-hidden', this.$form).val(val); + } + + registerUpdateListeners() { + this.debouncedUpdateUrl = _.debounce(() => this.updateUrl(), 200); + this.$urlInput.on('input', () => this.debouncedUpdateUrl()); + this.$protectedBranchesInput.on('change', () => this.updateProtectedBranches()); + this.$table.on('click', '.js-delete-mirror', event => this.deleteMirror(event)); + } + + togglePassword() { + const isPassword = this.$authMethod.val() === 'password'; + + if (!isPassword) { + this.$password.val(''); + this.updateUrl(); + } + this.$passwordGroup.collapse(isPassword ? 'show' : 'hide'); + } + + deleteMirror(event, existingPayload) { + const $target = $(event.currentTarget); + let payload = existingPayload; + + if (!payload) { + payload = { + project: { + remote_mirrors_attributes: { + id: $target.data('mirrorId'), + enabled: 0, + }, + }, + }; + } + + return axios + .put(this.mirrorEndpoint, payload) + .then(() => this.removeRow($target)) + .catch(() => Flash(__('Failed to remove mirror.'))); + } + + /* eslint-disable class-methods-use-this */ + removeRow($target) { + const row = $target.closest('tr'); + $('.js-delete-mirror', row).tooltip('hide'); + row.remove(); + } + /* eslint-enable class-methods-use-this */ +} diff --git a/app/assets/javascripts/project_select.js b/app/assets/javascripts/project_select.js index bce7556bd40..6f3b32f8eea 100644 --- a/app/assets/javascripts/project_select.js +++ b/app/assets/javascripts/project_select.js @@ -14,6 +14,7 @@ export default function projectSelect() { this.orderBy = $(select).data('orderBy') || 'id'; this.withIssuesEnabled = $(select).data('withIssuesEnabled'); this.withMergeRequestsEnabled = $(select).data('withMergeRequestsEnabled'); + this.allowClear = $(select).data('allowClear') || false; placeholder = "Search for project"; if (this.includeGroups) { @@ -71,6 +72,13 @@ export default function projectSelect() { text: function (project) { return project.name_with_namespace || project.name; }, + + initSelection: function(el, callback) { + return Api.project(el.val()).then(({ data }) => callback(data)); + }, + + allowClear: this.allowClear, + dropdownCssClass: "ajax-project-dropdown" }); if (simpleFilter) return select; diff --git a/app/assets/javascripts/reports/components/grouped_test_reports_app.vue b/app/assets/javascripts/reports/components/grouped_test_reports_app.vue index 140475b4dfa..7b37f4e9a97 100644 --- a/app/assets/javascripts/reports/components/grouped_test_reports_app.vue +++ b/app/assets/javascripts/reports/components/grouped_test_reports_app.vue @@ -1,10 +1,10 @@ <script> import { mapActions, mapGetters, mapState } from 'vuex'; import { s__ } from '~/locale'; - import { componentNames } from '~/vue_shared/components/reports/issue_body'; - import ReportSection from '~/vue_shared/components/reports/report_section.vue'; - import SummaryRow from '~/vue_shared/components/reports/summary_row.vue'; - import IssuesList from '~/vue_shared/components/reports/issues_list.vue'; + import { componentNames } from './issue_body'; + import ReportSection from './report_section.vue'; + import SummaryRow from './summary_row.vue'; + import IssuesList from './issues_list.vue'; import Modal from './modal.vue'; import createStore from '../store'; import { summaryTextBuilder, reportTextBuilder, statusIcon } from '../store/utils'; diff --git a/app/assets/javascripts/vue_shared/components/reports/issue_body.js b/app/assets/javascripts/reports/components/issue_body.js index 54dfb7b16bf..8b5af263d50 100644 --- a/app/assets/javascripts/vue_shared/components/reports/issue_body.js +++ b/app/assets/javascripts/reports/components/issue_body.js @@ -1,4 +1,4 @@ -import TestIssueBody from '~/reports/components/test_issue_body.vue'; +import TestIssueBody from './test_issue_body.vue'; export const components = { TestIssueBody, diff --git a/app/assets/javascripts/vue_shared/components/reports/issue_status_icon.vue b/app/assets/javascripts/reports/components/issue_status_icon.vue index f8189117ac3..85811698a37 100644 --- a/app/assets/javascripts/vue_shared/components/reports/issue_status_icon.vue +++ b/app/assets/javascripts/reports/components/issue_status_icon.vue @@ -1,11 +1,10 @@ <script> import Icon from '~/vue_shared/components/icon.vue'; - import { STATUS_FAILED, STATUS_NEUTRAL, STATUS_SUCCESS, -} from '~/vue_shared/components/reports/constants'; +} from '../constants'; export default { name: 'IssueStatusIcon', diff --git a/app/assets/javascripts/vue_shared/components/reports/issues_list.vue b/app/assets/javascripts/reports/components/issues_list.vue index 2545e84f932..df42201b5de 100644 --- a/app/assets/javascripts/vue_shared/components/reports/issues_list.vue +++ b/app/assets/javascripts/reports/components/issues_list.vue @@ -1,10 +1,10 @@ <script> -import IssuesBlock from '~/vue_shared/components/reports/report_issues.vue'; +import IssuesBlock from '~/reports/components/report_issues.vue'; import { STATUS_SUCCESS, STATUS_FAILED, STATUS_NEUTRAL, -} from '~/vue_shared/components/reports/constants'; +} from '~/reports/constants'; /** * Renders block of issues diff --git a/app/assets/javascripts/vue_shared/components/reports/modal_open_name.vue b/app/assets/javascripts/reports/components/modal_open_name.vue index 4f81cee2a38..4f81cee2a38 100644 --- a/app/assets/javascripts/vue_shared/components/reports/modal_open_name.vue +++ b/app/assets/javascripts/reports/components/modal_open_name.vue diff --git a/app/assets/javascripts/vue_shared/components/reports/report_issues.vue b/app/assets/javascripts/reports/components/report_issues.vue index 1f13e555b31..c553a374f66 100644 --- a/app/assets/javascripts/vue_shared/components/reports/report_issues.vue +++ b/app/assets/javascripts/reports/components/report_issues.vue @@ -1,6 +1,6 @@ <script> -import IssueStatusIcon from '~/vue_shared/components/reports/issue_status_icon.vue'; -import { components, componentNames } from '~/vue_shared/components/reports/issue_body'; +import IssueStatusIcon from '~/reports/components/issue_status_icon.vue'; +import { components, componentNames } from '~/reports/components/issue_body'; export default { name: 'ReportIssues', diff --git a/app/assets/javascripts/vue_shared/components/reports/report_link.vue b/app/assets/javascripts/reports/components/report_link.vue index 74d68f9f439..74d68f9f439 100644 --- a/app/assets/javascripts/vue_shared/components/reports/report_link.vue +++ b/app/assets/javascripts/reports/components/report_link.vue diff --git a/app/assets/javascripts/vue_shared/components/reports/report_section.vue b/app/assets/javascripts/reports/components/report_section.vue index a6dbf21092b..dc609d6f90e 100644 --- a/app/assets/javascripts/vue_shared/components/reports/report_section.vue +++ b/app/assets/javascripts/reports/components/report_section.vue @@ -1,8 +1,8 @@ <script> import { __ } from '~/locale'; import StatusIcon from '~/vue_merge_request_widget/components/mr_widget_status_icon.vue'; +import Popover from '~/vue_shared/components/help_popover.vue'; import IssuesList from './issues_list.vue'; -import Popover from '../help_popover.vue'; const LOADING = 'LOADING'; const ERROR = 'ERROR'; diff --git a/app/assets/javascripts/vue_shared/components/reports/summary_row.vue b/app/assets/javascripts/reports/components/summary_row.vue index 063beab58fc..4456d84c968 100644 --- a/app/assets/javascripts/vue_shared/components/reports/summary_row.vue +++ b/app/assets/javascripts/reports/components/summary_row.vue @@ -1,7 +1,7 @@ <script> import CiIcon from '~/vue_shared/components/ci_icon.vue'; import LoadingIcon from '~/vue_shared/components/loading_icon.vue'; -import Popover from '../help_popover.vue'; +import Popover from '~/vue_shared/components/help_popover.vue'; /** * Renders the summary row for each report diff --git a/app/assets/javascripts/reports/constants.js b/app/assets/javascripts/reports/constants.js index 807ecb1039e..c323dc543f3 100644 --- a/app/assets/javascripts/reports/constants.js +++ b/app/assets/javascripts/reports/constants.js @@ -11,6 +11,8 @@ export const SUCCESS = 'SUCCESS'; export const STATUS_FAILED = 'failed'; export const STATUS_SUCCESS = 'success'; +export const STATUS_NEUTRAL = 'neutral'; + export const ICON_WARNING = 'warning'; export const ICON_SUCCESS = 'success'; export const ICON_NOTFOUND = 'notfound'; diff --git a/app/assets/javascripts/sidebar/components/lock/edit_form_buttons.vue b/app/assets/javascripts/sidebar/components/lock/edit_form_buttons.vue index 5e7b8f9698f..63082654101 100644 --- a/app/assets/javascripts/sidebar/components/lock/edit_form_buttons.vue +++ b/app/assets/javascripts/sidebar/components/lock/edit_form_buttons.vue @@ -1,4 +1,5 @@ <script> +import { __ } from '~/locale'; import $ from 'jquery'; import eventHub from '../../event_hub'; @@ -17,7 +18,7 @@ export default { computed: { buttonText() { - return this.isLocked ? this.__('Unlock') : this.__('Lock'); + return this.isLocked ? __('Unlock') : __('Lock'); }, toggleLock() { diff --git a/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue b/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue index 8bbc59f623a..ab7fab7e5ca 100644 --- a/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue +++ b/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue @@ -1,5 +1,5 @@ <script> -import { __ } from '~/locale'; +import { __, sprintf } from '~/locale'; import Flash from '~/flash'; import tooltip from '~/vue_shared/directives/tooltip'; import issuableMixin from '~/vue_shared/mixins/issuable'; @@ -79,11 +79,9 @@ export default { .then(() => window.location.reload()) .catch(() => Flash( - this.__( - `Something went wrong trying to change the locked state of this ${ - this.issuableDisplayName - }`, - ), + sprintf(__('Something went wrong trying to change the locked state of this %{issuableDisplayName}'), { + issuableDisplayName: this.issuableDisplayName, + }), ), ); }, diff --git a/app/assets/javascripts/vue_shared/components/reports/constants.js b/app/assets/javascripts/vue_shared/components/reports/constants.js deleted file mode 100644 index dbde648bfdb..00000000000 --- a/app/assets/javascripts/vue_shared/components/reports/constants.js +++ /dev/null @@ -1,3 +0,0 @@ -export const STATUS_FAILED = 'failed'; -export const STATUS_SUCCESS = 'success'; -export const STATUS_NEUTRAL = 'neutral'; diff --git a/app/assets/stylesheets/bootstrap_migration.scss b/app/assets/stylesheets/bootstrap_migration.scss index c20738a20c3..e8e707cf90c 100644 --- a/app/assets/stylesheets/bootstrap_migration.scss +++ b/app/assets/stylesheets/bootstrap_migration.scss @@ -14,7 +14,6 @@ $border-radius-base: 3px !default; $modal-body-bg: $white-light; $input-border: $border-color; -$input-border-focus: $focus-border-color; $padding-base-vertical: $gl-vert-padding; $padding-base-horizontal: $gl-padding; @@ -86,7 +85,7 @@ strong { } a { - color: $gl-link-color; + color: $blue-600; } hr { diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss index b49db1f2e70..39ffabb3ea6 100644 --- a/app/assets/stylesheets/framework.scss +++ b/app/assets/stylesheets/framework.scss @@ -1,4 +1,5 @@ @import 'framework/variables'; +@import 'framework/variables_overrides'; @import 'framework/mixins'; @import 'bootstrap'; diff --git a/app/assets/stylesheets/framework/avatar.scss b/app/assets/stylesheets/framework/avatar.scss index 369556dc24e..4c7c399a3ca 100644 --- a/app/assets/stylesheets/framework/avatar.scss +++ b/app/assets/stylesheets/framework/avatar.scss @@ -103,6 +103,7 @@ display: flex; a { + width: 100%; display: flex; } diff --git a/app/assets/stylesheets/framework/awards.scss b/app/assets/stylesheets/framework/awards.scss index 8d11b92cf88..a265e4206f1 100644 --- a/app/assets/stylesheets/framework/awards.scss +++ b/app/assets/stylesheets/framework/awards.scss @@ -141,8 +141,8 @@ &:hover, &:active, &.is-active { - background-color: $row-hover; - border-color: $row-hover-border; + background-color: $blue-50; + border-color: $blue-200; box-shadow: none; outline: 0; diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss index 646cedd79ed..0dc7aa4ef68 100644 --- a/app/assets/stylesheets/framework/buttons.scss +++ b/app/assets/stylesheets/framework/buttons.scss @@ -350,7 +350,7 @@ &:focus { cursor: text; box-shadow: none; - border-color: lighten($dropdown-input-focus-border, 20%); + border-color: lighten($blue-300, 20%); color: $gray-darkest; background-color: $gray-light; } @@ -434,7 +434,7 @@ &:hover, &:active, &:focus { - color: $gl-link-color; + color: $blue-600; text-decoration: none; } } @@ -445,7 +445,7 @@ &:hover, &:active, &:focus { - color: $gl-link-color; + color: $blue-600; text-decoration: none; } } diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss index af17210f341..79ca6e61e9a 100644 --- a/app/assets/stylesheets/framework/common.scss +++ b/app/assets/stylesheets/framework/common.scss @@ -71,7 +71,7 @@ pre { } &.card.card-body-pre { - border: 1px solid $well-pre-bg; + border: 1px solid $gray-darker; background: $gray-light; border-radius: 0; color: $well-pre-color; @@ -114,7 +114,11 @@ hr { .item-title { font-weight: $gl-font-weight-bold; } .author-link { - color: $gl-link-color; + color: $blue-600; +} + +.author-link:hover { + text-decoration: none; } .back-link { @@ -229,7 +233,7 @@ li.note { .error-message { padding: 10px; - background: $error-bg; + background: $red-400; margin: 0; color: $white-light; @@ -240,11 +244,11 @@ li.note { } .warning_message { - border-left: 4px solid $warning-message-border; - color: $warning-message-color; + border-left: 4px solid $orange-200; + color: $orange-700; padding: 10px; margin-bottom: 10px; - background: $warning-message-bg; + background: $orange-100; padding-left: 20px; &.centered { @@ -344,15 +348,6 @@ img.emoji { } } -.profiler-results { - top: 73px !important; - - .profiler-button, - .profiler-controls { - border-color: $profiler-border !important; - } -} - .dropzone .dz-preview .dz-progress { border-color: $border-color !important; diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index eebce8b9011..83bc3776178 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -615,7 +615,7 @@ &:focus { color: $dropdown-link-color; - border-color: $dropdown-input-focus-border; + border-color: $blue-300; box-shadow: 0 0 4px $dropdown-input-focus-shadow; ~ .fa { diff --git a/app/assets/stylesheets/framework/emojis.scss b/app/assets/stylesheets/framework/emojis.scss index 3cde0490371..a8ec1e1145a 100644 --- a/app/assets/stylesheets/framework/emojis.scss +++ b/app/assets/stylesheets/framework/emojis.scss @@ -2,7 +2,7 @@ gl-emoji { font-style: normal; display: inline-flex; vertical-align: middle; - font-family: "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; + font-family: "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; font-size: 1.5em; line-height: 0.9; } diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss index 00eac1688f2..3bdf5bfc93a 100644 --- a/app/assets/stylesheets/framework/files.scss +++ b/app/assets/stylesheets/framework/files.scss @@ -112,7 +112,7 @@ &.image_file, &.video { - background: $file-image-bg; + background: $gray-darker; text-align: center; padding: 30px; @@ -131,7 +131,7 @@ } &.blob-no-preview { - background: $blob-bg; + background: $gray-darker; text-shadow: 0 1px 2px $white-light; padding: 100px 0; } @@ -146,7 +146,7 @@ } tr { - border-bottom: 1px solid $blame-border; + border-bottom: 1px solid $gray-darker; &:last-child { border-bottom: 0; @@ -211,7 +211,7 @@ } &.logs { - background: $logs-bg; + background: $gray-darker; max-height: 700px; overflow-y: auto; @@ -233,7 +233,7 @@ } &:hover { - background: $row-hover; + background: $blue-50; } } } @@ -286,19 +286,19 @@ span.idiff { .new-file { a { - color: $gl-text-green; + color: $green-600; } } .renamed-file { a { - color: $gl-text-orange; + color: $orange-600; } } .deleted-file { a { - color: $gl-text-red; + color: $red-500; } } diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss index 5d79610b21e..9b09ed0ed0a 100644 --- a/app/assets/stylesheets/framework/filters.scss +++ b/app/assets/stylesheets/framework/filters.scss @@ -205,7 +205,7 @@ &.focus, &.focus:hover { - border-color: $dropdown-input-focus-border; + border-color: $blue-300; box-shadow: 0 0 4px $search-input-focus-shadow-color; } @@ -294,7 +294,7 @@ &:hover, &:focus { color: $gl-text-color; - border-color: $dropdown-input-focus-border; + border-color: $blue-300; outline: none; } diff --git a/app/assets/stylesheets/framework/flash.scss b/app/assets/stylesheets/framework/flash.scss index e4bcb92876d..7a4c3914fb0 100644 --- a/app/assets/stylesheets/framework/flash.scss +++ b/app/assets/stylesheets/framework/flash.scss @@ -16,10 +16,10 @@ color: $gl-text-color; a { - color: $gl-link-color; + color: $blue-600; &:hover { - color: $gl-link-hover-color; + color: $blue-800; text-decoration: none; } } diff --git a/app/assets/stylesheets/framework/forms.scss b/app/assets/stylesheets/framework/forms.scss index d7149d93622..a70eece8f68 100644 --- a/app/assets/stylesheets/framework/forms.scss +++ b/app/assets/stylesheets/framework/forms.scss @@ -170,7 +170,7 @@ label { } .form-control::-webkit-input-placeholder { - color: $placeholder-text-color; + color: $gl-text-color-tertiary; } .input-group { @@ -201,7 +201,7 @@ label { } .gl-show-field-errors { - .form-control { + .form-control:not(textarea) { height: 34px; } diff --git a/app/assets/stylesheets/framework/issue_box.scss b/app/assets/stylesheets/framework/issue_box.scss index 86de88729ee..da5f80d9d37 100644 --- a/app/assets/stylesheets/framework/issue_box.scss +++ b/app/assets/stylesheets/framework/issue_box.scss @@ -39,7 +39,7 @@ } &.status-box-expired { - background-color: $issue-status-expired; + background-color: $orange-500; } &.status-box-upcoming { diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss index 4b67eab05b3..fdc0454d837 100644 --- a/app/assets/stylesheets/framework/lists.scss +++ b/app/assets/stylesheets/framework/lists.scss @@ -21,11 +21,11 @@ } &.disabled { - color: $list-text-disabled-color; + color: $gl-text-color-tertiary; } &:not(.ui-sort-disabled):hover { - background: $row-hover; + background: $blue-50; } &.unstyled { @@ -35,12 +35,12 @@ } &.warning-row { - background-color: $list-warning-row-bg; - border-color: $list-warning-row-border; - color: $list-warning-row-color; + background-color: $orange-100; + border-color: $orange-200; + color: $orange-700; &:hover { - background: $list-warning-row-bg; + background: $orange-100; } } @@ -73,7 +73,7 @@ } .card.card-body-title { - font-size: $list-font-size; + font-size: $gl-font-size; line-height: 18px; } } @@ -109,8 +109,8 @@ ul.content-list { li { border-color: $white-normal; - font-size: $list-font-size; - color: $list-text-color; + font-size: $gl-font-size; + color: $gl-text-color; &.no-description { .title { diff --git a/app/assets/stylesheets/framework/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss index 7290a174668..d8391b59a8c 100644 --- a/app/assets/stylesheets/framework/markdown_area.scss +++ b/app/assets/stylesheets/framework/markdown_area.scss @@ -179,7 +179,7 @@ &:hover, &:focus { svg { - fill: $gl-link-color; + fill: $blue-600; } } } diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss index 98bf26a5222..7edb89ce6f3 100644 --- a/app/assets/stylesheets/framework/mixins.scss +++ b/app/assets/stylesheets/framework/mixins.scss @@ -50,7 +50,7 @@ @include clearfix; padding: 10px 0; - border-bottom: 1px solid $list-border-light; + border-bottom: 1px solid $gray-darker; display: block; margin: 0; diff --git a/app/assets/stylesheets/framework/modal.scss b/app/assets/stylesheets/framework/modal.scss index ffb40166c15..7d53a631cdf 100644 --- a/app/assets/stylesheets/framework/modal.scss +++ b/app/assets/stylesheets/framework/modal.scss @@ -79,7 +79,6 @@ body.modal-open { .modal { background-color: $black-transparent; - z-index: 2100; @include media-breakpoint-up(md) { .modal-dialog { diff --git a/app/assets/stylesheets/framework/selects.scss b/app/assets/stylesheets/framework/selects.scss index b40dcf93969..88d2f0aaf85 100644 --- a/app/assets/stylesheets/framework/selects.scss +++ b/app/assets/stylesheets/framework/selects.scss @@ -153,7 +153,7 @@ transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s; &:focus { - border-color: $input-border-focus; + border-color: $blue-300; } &.select2-active { diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss index 473ca408c04..5c6110737a4 100644 --- a/app/assets/stylesheets/framework/typography.scss +++ b/app/assets/stylesheets/framework/typography.scss @@ -7,7 +7,7 @@ } a { - color: $md-link-color; + color: $blue-600; } img:not(.emoji) { @@ -180,7 +180,7 @@ } a > code { - color: $gl-link-color; + color: $blue-600; } dd { @@ -423,25 +423,25 @@ h4 { input, textarea { &::-webkit-input-placeholder { - color: $placeholder-text-color; + color: $gl-text-color-tertiary; } // support firefox 19+ vendor prefix &::-moz-placeholder { - color: $placeholder-text-color; + color: $gl-text-color-tertiary; opacity: 1; // FF defaults to 0.54 } // scss-lint:disable PseudoElement // support Edge vendor prefix &::-ms-input-placeholder { - color: $placeholder-text-color; + color: $gl-text-color-tertiary; } // scss-lint:disable PseudoElement // support IE vendor prefix &:-ms-input-placeholder { - color: $placeholder-text-color; + color: $gl-text-color-tertiary; } } diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 6a8ab737db0..7d92e271942 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -177,7 +177,6 @@ $border-gray-dark: darken($white-normal, $darken-border-factor); * UI elements */ $border-color: #e5e5e5; -$focus-border-color: $blue-300; $well-expand-item: #e8f2f7; $well-inner-border: #eef0f2; $well-light-border: #f1f1f1; @@ -196,38 +195,20 @@ $gl-text-color-quaternary: #d6d6d6; $gl-text-color-inverted: rgba(255, 255, 255, 1); $gl-text-color-secondary-inverted: rgba(255, 255, 255, 0.85); $gl-text-color-disabled: #919191; -$gl-text-green: $green-600; -$gl-text-green-hover: $green-700; -$gl-text-red: $red-500; -$gl-text-orange: $orange-600; -$gl-link-color: $blue-600; -$gl-link-hover-color: $blue-800; $gl-grayish-blue: #7f8fa4; -$gl-gray: $gl-text-color; $gl-gray-dark: #313236; $gl-gray-light: #5c5c5c; $gl-header-color: #4c4e54; -$gl-header-nav-hover-color: #434343; -$placeholder-text-color: $gl-text-color-tertiary; /* * Lists */ -$list-font-size: $gl-font-size; -$list-title-color: $gl-text-color; -$list-text-color: $gl-text-color; -$list-text-disabled-color: $gl-text-color-tertiary; -$list-border-light: #eee; $list-border: rgba(0, 0, 0, 0.05); $list-text-height: 42px; -$list-warning-row-bg: $orange-100; -$list-warning-row-border: $orange-200; -$list-warning-row-color: $orange-700; /* * Markdown */ -$md-link-color: $gl-link-color; $md-area-border: #ddd; /* @@ -259,8 +240,6 @@ $gl-bar-padding: 3px; /* * Misc */ -$row-hover: $blue-50; -$row-hover-border: $blue-200; $progress-color: #c0392b; $header-height: 40px; $ide-statusbar-height: 25px; @@ -268,19 +247,13 @@ $fixed-layout-width: 1280px; $limited-layout-width: 990px; $container-text-max-width: 540px; $gl-avatar-size: 40px; -$error-exclamation-point: $red-500; $border-radius-default: 4px; $border-radius-small: 2px; $settings-icon-size: 18px; -$provider-btn-not-active-color: $blue-500; -$link-underline-blue: $blue-500; -$active-item-blue: $blue-500; $layout-link-gray: #7e7c7c; $btn-side-margin: 10px; $btn-sm-side-margin: 7px; $btn-margin-5: 5px; -$issue-status-expired: $orange-500; -$issuable-sidebar-color: $gl-text-color-secondary; $sidebar-block-hover-color: #ebebeb; $group-path-color: #999; $namespace-kind-color: #aaa; @@ -303,7 +276,6 @@ $project-title-row-height: 24px; * Common component specific colors */ $hint-color: #999; -$well-pre-bg: #eee; $well-pre-color: #555; $loading-color: #555; $update-author-color: #999; @@ -312,10 +284,6 @@ $user-mention-bg-hover: rgba($blue-500, 0.15); $time-color: #999; $project-member-show-color: #aaa; $gl-promo-color: #aaa; -$error-bg: $red-400; -$warning-message-bg: $orange-100; -$warning-message-border: $orange-200; -$warning-message-color: $orange-700; $control-group-descr-color: #666; $table-permission-x-bg: #d9edf7; $username-color: #666; @@ -375,7 +343,7 @@ $diff-jagged-border-gradient-color: darken($white-normal, 8%); $monospace-font: 'Menlo', 'DejaVu Sans Mono', 'Liberation Mono', 'Consolas', 'Ubuntu Mono', 'Courier New', 'andale mono', 'lucida console', monospace; $regular-font: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu, Cantarell, - 'Helvetica Neue', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; + 'Helvetica Neue', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; /* * Dropdowns @@ -385,7 +353,7 @@ $dropdown-min-height: 40px; $dropdown-max-height: 312px; $dropdown-vertical-offset: 4px; $dropdown-link-color: #555; -$dropdown-link-hover-bg: $row-hover; +$dropdown-link-hover-bg: $blue-50; $dropdown-empty-row-bg: rgba(#000, 0.04); $dropdown-border-color: $border-color; $dropdown-shadow-color: rgba(#000, 0.1); @@ -393,8 +361,7 @@ $dropdown-divider-color: rgba(#000, 0.1); $dropdown-title-btn-color: #bfbfbf; $dropdown-input-color: #555; $dropdown-input-fa-color: #c7c7c7; -$dropdown-input-focus-border: $focus-border-color; -$dropdown-input-focus-shadow: rgba($dropdown-input-focus-border, 0.4); +$dropdown-input-focus-shadow: rgba($blue-300, 0.4); $dropdown-loading-bg: rgba(#fff, 0.6); $dropdown-chevron-size: 10px; $dropdown-toggle-active-border-color: darken($border-color, 14%); @@ -586,8 +553,8 @@ $commit-message-text-area-bg: rgba(0, 0, 0, 0); $common-gray: $gl-text-color; $common-gray-light: #bbb; $common-gray-dark: #444; -$common-red: $gl-text-red; -$common-green: $gl-text-green; +$common-red: $red-500; +$common-green: $green-600; /* * Editor @@ -604,11 +571,7 @@ $events-body-border: #ddd; /* * Files */ -$file-image-bg: #eee; -$blob-bg: #eee; -$blame-border: #eee; $blame-line-numbers-border: #ddd; -$logs-bg: #eee; $logs-li-color: #888; $logs-p-color: #333; @@ -679,7 +642,6 @@ $login-devise-error-color: $red-700; * Nav */ $nav-link-gray: #959494; -$nav-badge-bg: #eee; $nav-toggle-gray: #666; /* @@ -835,19 +797,3 @@ Prometheus $prometheus-table-row-highlight-color: $theme-gray-100; $priority-label-empty-state-width: 114px; - -/* - * Override Bootstrap 4 variables - */ - -$secondary: $gray-light; -$input-disabled-bg: $gray-light; -$input-border-color: $theme-gray-200; -$input-color: $gl-text-color; -$font-family-sans-serif: $regular-font; -$font-family-monospace: $monospace-font; -$input-line-height: 20px; -$btn-line-height: 20px; -$table-accent-bg: $gray-light; -$card-border-color: $border-color; -$card-cap-bg: $gray-light; diff --git a/app/assets/stylesheets/framework/variables_overrides.scss b/app/assets/stylesheets/framework/variables_overrides.scss new file mode 100644 index 00000000000..b9c343fa2e9 --- /dev/null +++ b/app/assets/stylesheets/framework/variables_overrides.scss @@ -0,0 +1,16 @@ +/* + * This file is only for overriding Bootstrap 4 variables. + * Please add any new variables to variables.scss + */ + +$secondary: $gray-light; +$input-disabled-bg: $gray-light; +$input-border-color: $theme-gray-200; +$input-color: $gl-text-color; +$font-family-sans-serif: $regular-font; +$font-family-monospace: $monospace-font; +$input-line-height: 20px; +$btn-line-height: 20px; +$table-accent-bg: $gray-light; +$card-border-color: $border-color; +$card-cap-bg: $gray-light; diff --git a/app/assets/stylesheets/framework/zen.scss b/app/assets/stylesheets/framework/zen.scss index dbd3144b9b4..f2d296fb875 100644 --- a/app/assets/stylesheets/framework/zen.scss +++ b/app/assets/stylesheets/framework/zen.scss @@ -44,7 +44,7 @@ color: $gl-text-color-secondary; &:hover { - color: $gl-link-color; + color: $blue-600; text-decoration: none; } } diff --git a/app/assets/stylesheets/page_bundles/ide.scss b/app/assets/stylesheets/page_bundles/ide.scss index 2b8163b8c68..eac1345742d 100644 --- a/app/assets/stylesheets/page_bundles/ide.scss +++ b/app/assets/stylesheets/page_bundles/ide.scss @@ -1158,7 +1158,7 @@ $ide-tree-text-start: $ide-activity-bar-width + $ide-tree-padding; } a { - color: $gl-link-color; + color: $blue-600; } } @@ -1293,6 +1293,7 @@ $ide-tree-text-start: $ide-activity-bar-width + $ide-tree-padding; &.build-page .top-bar { top: 0; + height: auto; font-size: 12px; border-top-right-radius: $border-radius-default; } diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss index a68b47b1d02..69d7de886b4 100644 --- a/app/assets/stylesheets/pages/boards.scss +++ b/app/assets/stylesheets/pages/boards.scss @@ -225,7 +225,7 @@ outline: 0; &:hover { - color: $gl-link-color; + color: $blue-600; } } @@ -288,7 +288,7 @@ &.is-active, &.is-active .board-card-assignee:hover a { - background-color: $row-hover; + background-color: $blue-50; } .badge { diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss index e8158cd7f6b..1696d18584d 100644 --- a/app/assets/stylesheets/pages/builds.scss +++ b/app/assets/stylesheets/pages/builds.scss @@ -221,7 +221,7 @@ color: $gl-text-color; &:hover { - color: $gl-link-color; + color: $blue-600; text-decoration: none; } } diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss index bce83bf0dd0..10764e0f3df 100644 --- a/app/assets/stylesheets/pages/commits.scss +++ b/app/assets/stylesheets/pages/commits.scss @@ -279,7 +279,7 @@ } &.autodevops-link { - color: $gl-link-color; + color: $blue-600; } } @@ -321,7 +321,7 @@ } .commit-sha { - color: $gl-link-color; + color: $blue-600; } .commit-row-message { diff --git a/app/assets/stylesheets/pages/cycle_analytics.scss b/app/assets/stylesheets/pages/cycle_analytics.scss index e2c0a7a6225..bc4c90711d7 100644 --- a/app/assets/stylesheets/pages/cycle_analytics.scss +++ b/app/assets/stylesheets/pages/cycle_analytics.scss @@ -165,7 +165,7 @@ border-right-color: transparent; border-top-color: $border-color; border-bottom-color: $border-color; - box-shadow: inset 2px 0 0 0 $active-item-blue; + box-shadow: inset 2px 0 0 0 $blue-500; .stage-name { font-weight: $gl-font-weight-bold; @@ -360,7 +360,7 @@ } .commit-sha { - color: $gl-link-color; + color: $blue-600; line-height: 1.3; vertical-align: top; font-weight: $gl-font-weight-normal; diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss index 591e21243ed..a999a70693e 100644 --- a/app/assets/stylesheets/pages/diff.scss +++ b/app/assets/stylesheets/pages/diff.scss @@ -186,7 +186,7 @@ } .image { - background: $file-image-bg; + background: $gray-darker; text-align: center; padding: 30px; @@ -511,13 +511,13 @@ padding: 0; background-color: transparent; border: 0; - color: $gl-link-color; + color: $blue-600; font-weight: $gl-font-weight-bold; &:hover, &:focus { outline: none; - color: $gl-link-hover-color; + color: $blue-800; } .caret-icon { diff --git a/app/assets/stylesheets/pages/editor.scss b/app/assets/stylesheets/pages/editor.scss index ddd1f8cc98a..892da152b5f 100644 --- a/app/assets/stylesheets/pages/editor.scss +++ b/app/assets/stylesheets/pages/editor.scss @@ -36,10 +36,7 @@ line-height: 35px; padding-top: 7px; padding-bottom: 7px; - - .float-right { - height: 20px; - } + display: flex; } .editor-ref { @@ -60,20 +57,18 @@ .new-file-name { display: inline-block; - max-width: 450px; + max-width: 420px; float: left; @media(max-width: map-get($grid-breakpoints, lg)-1) { - width: 280px; - } - - @media(max-width: map-get($grid-breakpoints, md)-1) { width: 180px; } } .file-buttons { - font-size: 0; + display: flex; + flex: 1; + justify-content: flex-end; } .select2 { @@ -111,12 +106,10 @@ } -@include media-breakpoint-down(xs) { +@include media-breakpoint-down(sm) { .file-editor { .file-title { - .float-right { - height: auto; - } + display: block; } .new-file-name { @@ -144,6 +137,10 @@ } } } + + .editor-ref { + max-width: 250px; + } } } diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss index 8a074017344..179c0964567 100644 --- a/app/assets/stylesheets/pages/environments.scss +++ b/app/assets/stylesheets/pages/environments.scss @@ -479,10 +479,10 @@ .deploy-info-text-link { font-family: $monospace-font; - fill: $gl-link-color; + fill: $blue-600; &:hover { - fill: $gl-link-hover-color; + fill: $blue-800; } } diff --git a/app/assets/stylesheets/pages/events.scss b/app/assets/stylesheets/pages/events.scss index f79586b68b9..da0c9b44498 100644 --- a/app/assets/stylesheets/pages/events.scss +++ b/app/assets/stylesheets/pages/events.scss @@ -6,7 +6,7 @@ font-size: $gl-font-size; padding: $gl-padding-top 0 $gl-padding-top 40px; border-bottom: 1px solid $white-normal; - color: $list-text-color; + color: $gl-text-color; position: relative; &.event-inline { @@ -58,7 +58,7 @@ .event-title { @include str-truncated(calc(100% - 174px)); font-weight: $gl-font-weight-bold; - color: $list-text-color; + color: $gl-text-color; } .event-body { diff --git a/app/assets/stylesheets/pages/graph.scss b/app/assets/stylesheets/pages/graph.scss index 49d8a5d959b..22fce893fd7 100644 --- a/app/assets/stylesheets/pages/graph.scss +++ b/app/assets/stylesheets/pages/graph.scss @@ -24,11 +24,11 @@ } .graph-additions { - color: $gl-text-green; + color: $green-600; } .graph-deletions { - color: $gl-text-red; + color: $red-500; } } diff --git a/app/assets/stylesheets/pages/groups.scss b/app/assets/stylesheets/pages/groups.scss index 1587aebfe1d..60b4d39bb1a 100644 --- a/app/assets/stylesheets/pages/groups.scss +++ b/app/assets/stylesheets/pages/groups.scss @@ -385,8 +385,8 @@ padding: $gl-padding-top; &:hover { - border-color: $row-hover-border; - background-color: $row-hover; + border-color: $blue-200; + background-color: $blue-50; cursor: pointer; } @@ -419,6 +419,7 @@ .stats { position: relative; line-height: normal; + text-align: right; flex-shrink: 0; > span { @@ -464,7 +465,7 @@ } .last-updated { - position: absolute; + position: relative; right: 12px; min-width: 250px; text-align: right; diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index 8e78d9f65eb..6f0f82964c8 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -141,7 +141,7 @@ color: inherit; &:hover { - color: $gl-link-hover-color; + color: $blue-800; .avatar { border-color: rgba($avatar-border, .2); @@ -241,7 +241,7 @@ &:hover { text-decoration: underline; - color: $gl-link-hover-color; + color: $blue-800; } } } @@ -329,7 +329,7 @@ } .btn-secondary-hover-link:hover { - color: $gl-link-color; + color: $blue-600; } .sidebar-collapsed-icon { @@ -423,10 +423,10 @@ width: 100%; text-align: center; margin-bottom: 10px; - color: $issuable-sidebar-color; + color: $gl-text-color-secondary; svg { - fill: $issuable-sidebar-color; + fill: $gl-text-color-secondary; } &:hover:not(.disabled), @@ -448,8 +448,8 @@ } .todo-undone { - color: $gl-link-color; - fill: $gl-link-color; + color: $blue-600; + fill: $blue-600; } .author { @@ -457,14 +457,14 @@ } .avatar-counter:hover { - color: $issuable-sidebar-color; - border-color: $issuable-sidebar-color; + color: $gl-text-color-secondary; + border-color: $gl-text-color-secondary; } .btn-clipboard { border: 0; background: transparent; - color: $issuable-sidebar-color; + color: $gl-text-color-secondary; &:hover { color: $gl-text-color; @@ -821,7 +821,7 @@ svg { width: 16px; height: 16px; - fill: $issuable-sidebar-color; + fill: $gl-text-color-secondary; } &:hover svg { diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss index 212e5979273..0f95fb911e1 100644 --- a/app/assets/stylesheets/pages/issues.scss +++ b/app/assets/stylesheets/pages/issues.scss @@ -157,7 +157,7 @@ ul.related-merge-requests > li { .issuable-email-modal-btn { padding: 0; - color: $gl-link-color; + color: $blue-600; background-color: transparent; border: 0; outline: 0; @@ -190,7 +190,7 @@ ul.related-merge-requests > li { .create-mr-dropdown-wrap { .ref::selection { - color: $placeholder-text-color; + color: $gl-text-color-tertiary; } .dropdown { diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss index 2b40404971c..d32943fceec 100644 --- a/app/assets/stylesheets/pages/labels.scss +++ b/app/assets/stylesheets/pages/labels.scss @@ -114,7 +114,7 @@ } &:hover { - color: $gl-link-color; + color: $blue-600; &.remove-row { color: $gl-danger; @@ -253,7 +253,7 @@ text-align: right; padding: 0; position: relative; - top: -3px; + margin: 0; } .label-badge { @@ -274,6 +274,7 @@ .label-links { list-style: none; + margin: 0; padding: 0; white-space: nowrap; } @@ -342,10 +343,10 @@ &.remove-row { &:hover { - color: $gl-text-red; + color: $red-500; svg { - fill: $gl-text-red; + fill: $red-500; } } } diff --git a/app/assets/stylesheets/pages/milestone.scss b/app/assets/stylesheets/pages/milestone.scss index 46437ce5841..1e92582d6d9 100644 --- a/app/assets/stylesheets/pages/milestone.scss +++ b/app/assets/stylesheets/pages/milestone.scss @@ -30,7 +30,7 @@ .milestone-progress { a { - color: $gl-link-color; + color: $blue-600; } } diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss index dcf590e7331..4f861d43f55 100644 --- a/app/assets/stylesheets/pages/note_form.scss +++ b/app/assets/stylesheets/pages/note_form.scss @@ -69,7 +69,7 @@ .comment-toolbar, .nav-links { - border-color: $focus-border-color; + border-color: $blue-300; } } @@ -306,7 +306,7 @@ &:hover, &:focus { - color: $gl-link-color; + color: $blue-600; outline: 0; } @@ -424,7 +424,7 @@ .uploading-error-icon, .uploading-error-message { - color: $gl-text-red; + color: $red-500; } .uploading-error-message { @@ -443,7 +443,7 @@ .attach-new-file, .button-attach-file, .retry-uploading-link { - color: $gl-link-color; + color: $blue-600; padding: 0; background: none; border: 0; @@ -452,5 +452,5 @@ } .markdown-selector { - color: $gl-link-color; + color: $blue-600; } diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index c369d89d63c..2e1b2126887 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -141,9 +141,6 @@ ul.notes { } .note-body { - overflow-x: auto; - overflow-y: hidden; - .note-text { @include md-typography; // Reset ul style types since we're nested inside a ul already @@ -210,7 +207,7 @@ ul.notes { } a { - color: $gl-link-color; + color: $blue-600; } p { @@ -253,14 +250,14 @@ ul.notes { overflow: hidden; .system-note-commit-list-toggler { - color: $gl-link-color; + color: $blue-600; padding: 10px 0 0; cursor: pointer; position: relative; z-index: 2; &:hover { - color: $gl-link-color; + color: $blue-600; text-decoration: underline; } } @@ -390,7 +387,7 @@ ul.notes { color: inherit; &:hover { - color: $gl-link-color; + color: $blue-600; } &:focus, @@ -451,7 +448,7 @@ ul.notes { .discussion-headline-light { a { - color: $gl-link-color; + color: $blue-600; } } @@ -560,12 +557,12 @@ ul.notes { &:hover, &.is-active { .danger-highlight { - color: $gl-text-red; + color: $red-500; } .link-highlight { - color: $gl-link-color; - fill: $gl-link-color; + color: $blue-600; + fill: $blue-600; } .award-control-icon-neutral { @@ -597,13 +594,13 @@ ul.notes { transition: color 0.1s linear; &:hover { - color: $gl-link-color; + color: $blue-600; } &:focus { text-decoration: underline; outline: none; - color: $gl-link-color; + color: $blue-600; } .fa { @@ -673,7 +670,7 @@ ul.notes { } a { - color: $gl-link-color; + color: $blue-600; } } @@ -759,16 +756,16 @@ ul.notes { &:not(.is-disabled) { &:hover, &:focus { - color: $gl-text-green; + color: $green-600; } } &.is-active { - color: $gl-text-green; + color: $green-600; &:hover, &:focus { - color: $gl-text-green-hover; + color: $green-700; } } diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index b68c89c25d8..ad057ed3c83 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -175,7 +175,7 @@ } .commit-sha { - color: $gl-link-color; + color: $blue-600; } .badge { diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss index b45e305897c..17f34319050 100644 --- a/app/assets/stylesheets/pages/profile.scss +++ b/app/assets/stylesheets/pages/profile.scss @@ -20,7 +20,7 @@ .account-btn-link, .profile-settings-sidebar a, .settings-sidebar a { - color: $md-link-color; + color: $blue-600; } .private-tokens-reset div.reset-action:not(:first-child) { @@ -137,7 +137,7 @@ .profile-settings-content { a { - color: $md-link-color; + color: $blue-600; } } @@ -170,7 +170,7 @@ background-color: $gray-light; &.not-active { - color: $provider-btn-not-active-color; + color: $blue-500; } } diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index d4a39e02cb4..cb7663c09a5 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -411,7 +411,7 @@ line-height: $gl-btn-line-height; &:hover { - color: $gl-link-color; + color: $blue-600; } } } @@ -472,8 +472,8 @@ &:hover:not(.disabled), &.forked { - background-color: $row-hover; - border-color: $row-hover-border; + background-color: $blue-50; + border-color: $blue-200; } .avatar-container, @@ -796,8 +796,19 @@ } .repository-languages-bar { - height: 6px; + height: 8px; margin-bottom: $gl-padding-8; + background-color: $white-light; + border-radius: $border-radius-default; + + .progress-bar { + margin-right: 2px; + padding: 0 $gl-padding-4; + + &:last-child { + margin-right: 0; + } + } } pre.light-well { @@ -865,10 +876,6 @@ pre.light-well { .avatar-container { align-self: flex-start; - - > a { - width: 100%; - } } .project-details { @@ -929,7 +936,7 @@ pre.light-well { .cannot-be-merged, .cannot-be-merged:hover { - color: $error-exclamation-point; + color: $red-500; margin-top: 2px; } @@ -996,7 +1003,7 @@ pre.light-well { margin-left: 5px; &.is-done { - color: $gl-text-green; + color: $green-600; } } diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss index 60b280fd12e..5b3a468cd1c 100644 --- a/app/assets/stylesheets/pages/search.scss +++ b/app/assets/stylesheets/pages/search.scss @@ -23,7 +23,7 @@ $search-avatar-size: 16px; .search-text-input:hover, .form-control:hover, :not[readonly] { - border-color: lighten($dropdown-input-focus-border, 20%); + border-color: lighten($blue-300, 20%); box-shadow: 0 0 4px lighten($search-input-focus-shadow-color, 20%); } @@ -127,7 +127,7 @@ input[type='checkbox']:hover { &.search-active { form { @extend .form-control:focus; - border-color: $dropdown-input-focus-border; + border-color: $blue-300; box-shadow: none; @include media-breakpoint-up(xl) { @@ -259,6 +259,6 @@ input[type='checkbox']:hover { &:hover, &:focus { - color: $gl-link-color; + color: $blue-600; } } diff --git a/app/assets/stylesheets/pages/settings.scss b/app/assets/stylesheets/pages/settings.scss index 839ac5ba59b..5aa4cdec9c3 100644 --- a/app/assets/stylesheets/pages/settings.scss +++ b/app/assets/stylesheets/pages/settings.scss @@ -113,9 +113,9 @@ .settings-message { padding: 5px; line-height: 1.3; - color: $warning-message-color; - background-color: $warning-message-bg; - border: 1px solid $warning-message-border; + color: $orange-700; + background-color: $orange-100; + border: 1px solid $orange-200; border-radius: $border-radius-base; } @@ -301,3 +301,17 @@ margin-bottom: 0; } } + +.mirror-error-badge { + background-color: $red-400; + border-radius: $border-radius-default; + color: $white-light; +} + +.push-pull-table { + margin-top: 1em; + + .mirror-action-buttons { + padding-right: 0; + } +} diff --git a/app/assets/stylesheets/pages/todos.scss b/app/assets/stylesheets/pages/todos.scss index 010a2c05a1c..5d3b7b21ce4 100644 --- a/app/assets/stylesheets/pages/todos.scss +++ b/app/assets/stylesheets/pages/todos.scss @@ -12,8 +12,8 @@ flex-direction: row; &:hover { - background-color: $row-hover; - border-color: $row-hover-border; + background-color: $blue-50; + border-color: $blue-200; cursor: pointer; } @@ -22,7 +22,7 @@ border-bottom: 1px solid transparent; &:hover { - border-color: $row-hover-border; + border-color: $blue-200; } } diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss index 1cc26d40ba9..dc5ca78ff58 100644 --- a/app/assets/stylesheets/pages/tree.scss +++ b/app/assets/stylesheets/pages/tree.scss @@ -111,9 +111,9 @@ &:hover:not(.tree-truncated-warning) { td { - background-color: $row-hover; - border-top: 1px solid $row-hover-border; - border-bottom: 1px solid $row-hover-border; + background-color: $blue-50; + border-top: 1px solid $blue-200; + border-bottom: 1px solid $blue-200; cursor: pointer; } } @@ -229,7 +229,7 @@ .upload-link { font-weight: $gl-font-weight-normal; - color: $md-link-color; + color: $blue-600; } .repo-charts { diff --git a/app/assets/stylesheets/performance_bar.scss b/app/assets/stylesheets/performance_bar.scss index 7a93c4dfa28..57d43beaf21 100644 --- a/app/assets/stylesheets/performance_bar.scss +++ b/app/assets/stylesheets/performance_bar.scss @@ -6,7 +6,7 @@ left: 0; top: 0; width: 100%; - z-index: 2000; + z-index: 1039; height: $performance-bar-height; background: $black; diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 05ed3669a41..e5b38898a67 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -11,7 +11,6 @@ class ApplicationController < ActionController::Base include EnforcesTwoFactorAuthentication include WithPerformanceBar - before_action :limit_unauthenticated_session_times before_action :authenticate_sessionless_user! before_action :authenticate_user! before_action :enforce_terms!, if: :should_enforce_terms? @@ -27,6 +26,7 @@ class ApplicationController < ActionController::Base around_action :set_locale after_action :set_page_title_header, if: :json_request? + after_action :limit_unauthenticated_session_times protect_from_forgery with: :exception, prepend: true diff --git a/app/controllers/autocomplete_controller.rb b/app/controllers/autocomplete_controller.rb index 86bade49ec9..9e30b982b06 100644 --- a/app/controllers/autocomplete_controller.rb +++ b/app/controllers/autocomplete_controller.rb @@ -1,67 +1,39 @@ class AutocompleteController < ApplicationController - AWARD_EMOJI_MAX = 100 - skip_before_action :authenticate_user!, only: [:users, :award_emojis] - before_action :load_project, only: [:users] - before_action :load_group, only: [:users] def users - @users = AutocompleteUsersFinder.new(params: params, current_user: current_user, project: @project, group: @group).execute - - render json: UserSerializer.new.represent(@users) - end - - def user - @user = User.find(params[:id]) - render json: UserSerializer.new.represent(@user) - end - - def projects - project = Project.find_by_id(params[:project_id]) - projects = projects_finder.execute(project, search: params[:search], offset_id: params[:offset_id]) + project = Autocomplete::ProjectFinder + .new(current_user, params) + .execute - render json: projects.to_json(only: [:id, :name_with_namespace], methods: :name_with_namespace) - end + group = Autocomplete::GroupFinder + .new(current_user, project, params) + .execute - def award_emojis - emoji_with_count = AwardEmoji - .limit(AWARD_EMOJI_MAX) - .where(user: current_user) - .group(:name) - .order('count_all DESC, name ASC') - .count + users = Autocomplete::UsersFinder + .new(params: params, current_user: current_user, project: project, group: group) + .execute - # Transform from hash to array to guarantee json order - # e.g. { 'thumbsup' => 2, 'thumbsdown' = 1 } - # => [{ name: 'thumbsup' }, { name: 'thumbsdown' }] - render json: emoji_with_count.map { |k, v| { name: k } } + render json: UserSerializer.new.represent(users) end - private - - def load_group - @group ||= begin - if @project.blank? && params[:group_id].present? - group = Group.find(params[:group_id]) - return render_404 unless can?(current_user, :read_group, group) + def user + user = UserFinder.new(params).execute! - group - end - end + render json: UserSerializer.new.represent(user) end - def load_project - @project ||= begin - if params[:project_id].present? - project = Project.find(params[:project_id]) - return render_404 unless can?(current_user, :read_project, project) + # Displays projects to use for the dropdown when moving a resource from one + # project to another. + def projects + projects = Autocomplete::MoveToProjectFinder + .new(current_user, params) + .execute - project - end - end + render json: MoveToProjectSerializer.new.represent(projects) end - def projects_finder - MoveToProjectFinder.new(current_user) + def award_emojis + render json: AwardedEmojiFinder.new(current_user).execute end end diff --git a/app/controllers/notification_settings_controller.rb b/app/controllers/notification_settings_controller.rb index ed20302487c..461f26561f1 100644 --- a/app/controllers/notification_settings_controller.rb +++ b/app/controllers/notification_settings_controller.rb @@ -5,14 +5,14 @@ class NotificationSettingsController < ApplicationController return render_404 unless can_read?(resource) @notification_setting = current_user.notification_settings_for(resource) - @saved = @notification_setting.update(notification_setting_params) + @saved = @notification_setting.update(notification_setting_params_for(resource)) render_response end def update @notification_setting = current_user.notification_settings.find(params[:id]) - @saved = @notification_setting.update(notification_setting_params) + @saved = @notification_setting.update(notification_setting_params_for(@notification_setting.source)) render_response end @@ -42,8 +42,8 @@ class NotificationSettingsController < ApplicationController } end - def notification_setting_params - allowed_fields = NotificationSetting::EMAIL_EVENTS.dup + def notification_setting_params_for(source) + allowed_fields = NotificationSetting.email_events(source).dup allowed_fields << :level params.require(:notification_setting).permit(allowed_fields) end diff --git a/app/controllers/projects/autocomplete_sources_controller.rb b/app/controllers/projects/autocomplete_sources_controller.rb index 07627ffb69f..a8f73ed5cb0 100644 --- a/app/controllers/projects/autocomplete_sources_controller.rb +++ b/app/controllers/projects/autocomplete_sources_controller.rb @@ -32,13 +32,8 @@ class Projects::AutocompleteSourcesController < Projects::ApplicationController end def target - case params[:type]&.downcase - when 'issue' - IssuesFinder.new(current_user, project_id: @project.id).find_by(iid: params[:type_id]) - when 'mergerequest' - MergeRequestsFinder.new(current_user, project_id: @project.id).find_by(iid: params[:type_id]) - when 'commit' - @project.commit(params[:type_id]) - end + QuickActions::TargetService + .new(project, current_user) + .execute(params[:type], params[:type_id]) end end diff --git a/app/controllers/projects/pages_controller.rb b/app/controllers/projects/pages_controller.rb index cae6e2c40b8..ff49911d892 100644 --- a/app/controllers/projects/pages_controller.rb +++ b/app/controllers/projects/pages_controller.rb @@ -11,7 +11,7 @@ class Projects::PagesController < Projects::ApplicationController def destroy project.remove_pages - project.pages_domains.destroy_all + project.pages_domains.destroy_all # rubocop: disable DestroyAll respond_to do |format| format.html do diff --git a/app/finders/autocomplete/group_finder.rb b/app/finders/autocomplete/group_finder.rb new file mode 100644 index 00000000000..dd97ac4c817 --- /dev/null +++ b/app/finders/autocomplete/group_finder.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +module Autocomplete + # Finder for retrieving a group to use for autocomplete data sources. + class GroupFinder + attr_reader :current_user, :project, :group_id + + # current_user - The currently logged in user, if any. + # project - The Project (if any) to use for the autocomplete data sources. + # params - A Hash containing parameters to use for finding the project. + # + # The following parameters are supported: + # + # * group_id: The ID of the group to find. + def initialize(current_user = nil, project = nil, params = {}) + @current_user = current_user + @project = project + @group_id = params[:group_id] + end + + # Attempts to find a Group based on the current group ID. + def execute + return unless project.blank? && group_id.present? + + group = Group.find(group_id) + + # This removes the need for using `return render_404` and similar patterns + # in controllers that use this finder. + unless Ability.allowed?(current_user, :read_group, group) + raise ActiveRecord::RecordNotFound + .new("Could not find a Group with ID #{group_id}") + end + + group + end + end +end diff --git a/app/finders/autocomplete/move_to_project_finder.rb b/app/finders/autocomplete/move_to_project_finder.rb new file mode 100644 index 00000000000..edaf74c5f92 --- /dev/null +++ b/app/finders/autocomplete/move_to_project_finder.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +module Autocomplete + # Finder that retrieves a list of projects that an issue can be moved to. + class MoveToProjectFinder + attr_reader :current_user, :search, :project_id, :offset_id + + # current_user - The User object of the user that wants to view the list of + # projects. + # + # params - A Hash containing additional parameters to set. + # + # The following parameters can be set (as Symbols): + # + # * search: An optional search query to apply to the list of projects. + # * project_id: The ID of a project to exclude from the returned relation. + # * offset_id: The ID of a project to use for pagination. When given, only + # projects with a lower ID are included in the list. + def initialize(current_user, params = {}) + @current_user = current_user + @search = params[:search] + @project_id = params[:project_id] + @offset_id = params[:offset_id] + end + + def execute + current_user + .projects_where_can_admin_issues + .optionally_search(search) + .excluding_project(project_id) + .paginate_in_descending_order_using_id(before: offset_id) + .eager_load_namespace_and_owner + end + end +end diff --git a/app/finders/autocomplete/project_finder.rb b/app/finders/autocomplete/project_finder.rb new file mode 100644 index 00000000000..3a4696f4c2e --- /dev/null +++ b/app/finders/autocomplete/project_finder.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +module Autocomplete + # Finder for retrieving a project to use for autocomplete data sources. + class ProjectFinder + attr_reader :current_user, :project_id + + # current_user - The currently logged in user, if any. + # params - A Hash containing parameters to use for finding the project. + # + # The following parameters are supported: + # + # * project_id: The ID of the project to find. + def initialize(current_user = nil, params = {}) + @current_user = current_user + @project_id = params[:project_id] + end + + # Attempts to find a Project based on the current project ID. + def execute + return if project_id.blank? + + project = Project.find(project_id) + + # This removes the need for using `return render_404` and similar patterns + # in controllers that use this finder. + unless Ability.allowed?(current_user, :read_project, project) + raise ActiveRecord::RecordNotFound + .new("Could not find a Project with ID #{project_id}") + end + + project + end + end +end diff --git a/app/finders/autocomplete/users_finder.rb b/app/finders/autocomplete/users_finder.rb new file mode 100644 index 00000000000..b2557469079 --- /dev/null +++ b/app/finders/autocomplete/users_finder.rb @@ -0,0 +1,85 @@ +# frozen_string_literal: true + +module Autocomplete + class UsersFinder + # The number of users to display in the results is hardcoded to 20, and + # pagination is not supported. This ensures that performance remains + # consistent and removes the need for implementing keyset pagination to + # ensure good performance. + LIMIT = 20 + + attr_reader :current_user, :project, :group, :search, :skip_users, + :author_id, :todo_filter, :todo_state_filter, + :filter_by_current_user + + def initialize(params:, current_user:, project:, group:) + @current_user = current_user + @project = project + @group = group + @search = params[:search] + @skip_users = params[:skip_users] + @author_id = params[:author_id] + @todo_filter = params[:todo_filter] + @todo_state_filter = params[:todo_state_filter] + @filter_by_current_user = params[:current_user] + end + + def execute + items = limited_users + + if search.blank? + # Include current user if available to filter by "Me" + items.unshift(current_user) if prepend_current_user? + + if prepend_author? && (author = User.find_by_id(author_id)) + items.unshift(author) + end + end + + items.uniq + end + + private + + # Returns the users based on the input parameters, as an Array. + # + # This method is separate so it is easier to extend in EE. + def limited_users + # When changing the order of these method calls, make sure that + # reorder_by_name() is called _before_ optionally_search(), otherwise + # reorder_by_name will break the ORDER BY applied in optionally_search(). + find_users + .active + .reorder_by_name + .optionally_search(search) + .where_not_in(skip_users) + .limit_to_todo_authors( + user: current_user, + with_todos: todo_filter, + todo_state: todo_state_filter + ) + .limit(LIMIT) + .to_a + end + + def prepend_current_user? + filter_by_current_user.present? && current_user + end + + def prepend_author? + author_id.present? && current_user + end + + def find_users + if project + project.authorized_users.union_with_user(author_id) + elsif group + group.users_with_parents + elsif current_user + User.all + else + User.none + end + end + end +end diff --git a/app/finders/autocomplete_users_finder.rb b/app/finders/autocomplete_users_finder.rb deleted file mode 100644 index e8a03947f59..00000000000 --- a/app/finders/autocomplete_users_finder.rb +++ /dev/null @@ -1,68 +0,0 @@ -class AutocompleteUsersFinder - # The number of users to display in the results is hardcoded to 20, and - # pagination is not supported. This ensures that performance remains - # consistent and removes the need for implementing keyset pagination to ensure - # good performance. - LIMIT = 20 - - attr_reader :current_user, :project, :group, :search, :skip_users, - :author_id, :params - - def initialize(params:, current_user:, project:, group:) - @current_user = current_user - @project = project - @group = group - @search = params[:search] - @skip_users = params[:skip_users] - @author_id = params[:author_id] - @params = params - end - - def execute - items = find_users - items = items.active - items = items.reorder(:name) - items = items.search(search) if search.present? - items = items.where.not(id: skip_users) if skip_users.present? - items = items.limit(LIMIT) - - if params[:todo_filter].present? && current_user - items = items.todo_authors(current_user.id, params[:todo_state_filter]) - end - - if search.blank? - # Include current user if available to filter by "Me" - if params[:current_user].present? && current_user - items = [current_user, *items].uniq - end - - if author_id.present? && current_user - author = User.find_by_id(author_id) - items = [author, *items].uniq if author - end - end - - items - end - - private - - def find_users - return users_from_project if project - return group.users_with_parents if group - return User.all if current_user - - User.none - end - - def users_from_project - if author_id.present? - union = Gitlab::SQL::Union - .new([project.authorized_users, User.where(id: author_id)]) - - User.from("(#{union.to_sql}) #{User.table_name}") - else - project.authorized_users - end - end -end diff --git a/app/finders/awarded_emoji_finder.rb b/app/finders/awarded_emoji_finder.rb new file mode 100644 index 00000000000..f0cc17f3b26 --- /dev/null +++ b/app/finders/awarded_emoji_finder.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +# Class for retrieving information about emoji awarded _by_ a particular user. +class AwardedEmojiFinder + attr_reader :current_user + + # current_user - The User to generate the data for. + def initialize(current_user = nil) + @current_user = current_user + end + + def execute + return [] unless current_user + + # We want the resulting data set to be an Array containing the emoji names + # in descending order, based on how often they were awarded. + AwardEmoji + .award_counts_for_user(current_user) + .map { |name, _| { name: name } } + end +end diff --git a/app/finders/license_template_finder.rb b/app/finders/license_template_finder.rb new file mode 100644 index 00000000000..fad33f0eca2 --- /dev/null +++ b/app/finders/license_template_finder.rb @@ -0,0 +1,36 @@ +# LicenseTemplateFinder +# +# Used to find license templates, which may come from a variety of external +# sources +# +# Arguments: +# popular: boolean. When set to true, only "popular" licenses are shown. When +# false, all licenses except popular ones are shown. When nil (the +# default), *all* licenses will be shown. +class LicenseTemplateFinder + attr_reader :params + + def initialize(params = {}) + @params = params + end + + def execute + Licensee::License.all(featured: popular_only?).map do |license| + LicenseTemplate.new( + id: license.key, + name: license.name, + nickname: license.nickname, + category: (license.featured? ? :Popular : :Other), + content: license.content, + url: license.url, + meta: license.meta + ) + end + end + + private + + def popular_only? + params.fetch(:popular, nil) + end +end diff --git a/app/finders/move_to_project_finder.rb b/app/finders/move_to_project_finder.rb deleted file mode 100644 index 038d5565a1e..00000000000 --- a/app/finders/move_to_project_finder.rb +++ /dev/null @@ -1,21 +0,0 @@ -class MoveToProjectFinder - PAGE_SIZE = 50 - - def initialize(user) - @user = user - end - - def execute(from_project, search: nil, offset_id: nil) - projects = @user.projects_where_can_admin_issues - projects = projects.search(search) if search.present? - projects = projects.excluding_project(from_project) - projects = projects.order_id_desc - - # infinite scroll using offset - projects = projects.where('projects.id < ?', offset_id) if offset_id.present? - projects = projects.limit(PAGE_SIZE) - - # to ask for Project#name_with_namespace - projects.includes(namespace: :owner) - end -end diff --git a/app/finders/user_finder.rb b/app/finders/user_finder.rb new file mode 100644 index 00000000000..484a93c9873 --- /dev/null +++ b/app/finders/user_finder.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +# A simple finding for obtaining a single User. +# +# While using `User.find_by` directly is straightforward, it can lead to a lot +# of code duplication. Sometimes we just want to find a user by an ID, other +# times we may want to exclude blocked user. By using this finder (and extending +# it whenever necessary) we can keep this logic in one place. +class UserFinder + attr_reader :params + + def initialize(params) + @params = params + end + + # Tries to find a User, returning nil if none could be found. + def execute + User.find_by(id: params[:id]) + end + + # Tries to find a User, raising a `ActiveRecord::RecordNotFound` if it could + # not be found. + def execute! + User.find(params[:id]) + end +end diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb index 2bdf2c2c120..1e05f07e676 100644 --- a/app/helpers/application_settings_helper.rb +++ b/app/helpers/application_settings_helper.rb @@ -254,6 +254,7 @@ module ApplicationSettingsHelper :usage_ping_enabled, :instance_statistics_visibility_private, :user_default_external, + :user_show_add_ssh_key_message, :user_oauth_applications, :version_check_enabled, :web_ide_clientside_preview_enabled diff --git a/app/helpers/avatars_helper.rb b/app/helpers/avatars_helper.rb index d48dae8f06d..494f785e305 100644 --- a/app/helpers/avatars_helper.rb +++ b/app/helpers/avatars_helper.rb @@ -1,28 +1,10 @@ module AvatarsHelper def project_icon(project_id, options = {}) - project = - if project_id.respond_to?(:avatar_url) - project_id - else - Project.find_by_full_path(project_id) - end - - if project.avatar_url - image_tag project.avatar_url, options - else # generated icon - project_identicon(project, options) - end + source_icon(Project, project_id, options) end - def project_identicon(project, options = {}) - bg_key = (project.id % 7) + 1 - options[:class] ||= '' - options[:class] << ' identicon' - options[:class] << " bg#{bg_key}" - - content_tag(:div, class: options[:class]) do - project.name[0, 1].upcase - end + def group_icon(group_id, options = {}) + source_icon(Group, group_id, options) end # Takes both user and email and returns the avatar_icon by @@ -123,4 +105,32 @@ module AvatarsHelper mail_to(options[:user_email], avatar) end end + + private + + def source_icon(klass, source_id, options = {}) + source = + if source_id.respond_to?(:avatar_url) + source_id + else + klass.find_by_full_path(source_id) + end + + if source.avatar_url + image_tag source.avatar_url, options + else + source_identicon(source, options) + end + end + + def source_identicon(source, options = {}) + bg_key = (source.id % 7) + 1 + options[:class] ||= '' + options[:class] << ' identicon' + options[:class] << " bg#{bg_key}" + + content_tag(:div, class: options[:class].strip) do + source.name[0, 1].upcase + end + end end diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb index 7eb45ddd117..b61cbd5418a 100644 --- a/app/helpers/blob_helper.rb +++ b/app/helpers/blob_helper.rb @@ -182,12 +182,14 @@ module BlobHelper def licenses_for_select return @licenses_for_select if defined?(@licenses_for_select) - licenses = Licensee::License.all + grouped_licenses = LicenseTemplateFinder.new.execute.group_by(&:category) + categories = grouped_licenses.keys - @licenses_for_select = { - Popular: licenses.select(&:featured).map { |license| { name: license.name, id: license.key } }, - Other: licenses.reject(&:featured).map { |license| { name: license.name, id: license.key } } - } + @licenses_for_select = categories.each_with_object({}) do |category, hash| + hash[category] = grouped_licenses[category].map do |license| + { name: license.name, id: license.id } + end + end end def ref_project diff --git a/app/helpers/button_helper.rb b/app/helpers/button_helper.rb index 0171a880164..7adc882bc47 100644 --- a/app/helpers/button_helper.rb +++ b/app/helpers/button_helper.rb @@ -73,7 +73,11 @@ module ButtonHelper end def ssh_clone_button(project, append_link: true) - dropdown_description = _("You won't be able to pull or push project code via SSH until you add an SSH key to your profile") if current_user.try(:require_ssh_key?) + if Gitlab::CurrentSettings.user_show_add_ssh_key_message? && + current_user.try(:require_ssh_key?) + dropdown_description = _("You won't be able to pull or push project code via SSH until you add an SSH key to your profile") + end + append_url = project.ssh_url_to_repo if append_link dropdown_item_with_description('SSH', dropdown_description, href: append_url) diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb index 3c5c8bbd71b..5b51d2f2425 100644 --- a/app/helpers/groups_helper.rb +++ b/app/helpers/groups_helper.rb @@ -33,11 +33,6 @@ module GroupsHelper .count end - def group_icon(group, options = {}) - img_path = group_icon_url(group, options) - image_tag img_path, options - end - def group_icon_url(group, options = {}) if group.is_a?(String) group = Group.find_by_full_path(group) diff --git a/app/helpers/icons_helper.rb b/app/helpers/icons_helper.rb index 60acb5c740f..a5612372aa6 100644 --- a/app/helpers/icons_helper.rb +++ b/app/helpers/icons_helper.rb @@ -62,6 +62,8 @@ module IconsHelper names = "key" when "two-factor" names = "key" + when "google_oauth2" + names = "google" end options.include?(:base) ? fa_stacked_icon(names, options) : fa_icon(names, options) diff --git a/app/helpers/mirror_helper.rb b/app/helpers/mirror_helper.rb new file mode 100644 index 00000000000..93ed22513ac --- /dev/null +++ b/app/helpers/mirror_helper.rb @@ -0,0 +1,5 @@ +module MirrorHelper + def mirrors_form_data_attributes + { project_mirror_endpoint: project_mirror_path(@project) } + end +end diff --git a/app/helpers/notifications_helper.rb b/app/helpers/notifications_helper.rb index 3e42063224e..a185f2916d4 100644 --- a/app/helpers/notifications_helper.rb +++ b/app/helpers/notifications_helper.rb @@ -83,21 +83,11 @@ module NotificationsHelper end def notification_event_name(event) - # All values from NotificationSetting::EMAIL_EVENTS + # All values from NotificationSetting.email_events case event when :success_pipeline s_('NotificationEvent|Successful pipeline') else - N_('NotificationEvent|New note') - N_('NotificationEvent|New issue') - N_('NotificationEvent|Reopen issue') - N_('NotificationEvent|Close issue') - N_('NotificationEvent|Reassign issue') - N_('NotificationEvent|New merge request') - N_('NotificationEvent|Close merge request') - N_('NotificationEvent|Reassign merge request') - N_('NotificationEvent|Merge merge request') - N_('NotificationEvent|Failed pipeline') s_(event.to_s.humanize) end end diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 4b1c6a8c1a4..1a08829b45a 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -192,7 +192,10 @@ module ProjectsHelper end def show_no_ssh_key_message? - cookies[:hide_no_ssh_message].blank? && !current_user.hide_no_ssh_key && current_user.require_ssh_key? + Gitlab::CurrentSettings.user_show_add_ssh_key_message? && + cookies[:hide_no_ssh_message].blank? && + !current_user.hide_no_ssh_key && + current_user.require_ssh_key? end def show_no_password_message? diff --git a/app/mailers/abuse_report_mailer.rb b/app/mailers/abuse_report_mailer.rb index fe5f68ba3d5..e032f568913 100644 --- a/app/mailers/abuse_report_mailer.rb +++ b/app/mailers/abuse_report_mailer.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class AbuseReportMailer < BaseMailer def notify(abuse_report_id) return unless deliverable? diff --git a/app/mailers/base_mailer.rb b/app/mailers/base_mailer.rb index 654468bc7fe..5fd209c4761 100644 --- a/app/mailers/base_mailer.rb +++ b/app/mailers/base_mailer.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class BaseMailer < ActionMailer::Base around_action :render_with_default_locale diff --git a/app/mailers/devise_mailer.rb b/app/mailers/devise_mailer.rb index 962570a0efd..7aa75ee30e6 100644 --- a/app/mailers/devise_mailer.rb +++ b/app/mailers/devise_mailer.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class DeviseMailer < Devise::Mailer default from: "#{Gitlab.config.gitlab.email_display_name} <#{Gitlab.config.gitlab.email_from}>" default reply_to: Gitlab.config.gitlab.email_reply_to @@ -9,8 +11,9 @@ class DeviseMailer < Devise::Mailer protected def subject_for(key) - subject = super - subject << " | #{Gitlab.config.gitlab.email_subject_suffix}" if Gitlab.config.gitlab.email_subject_suffix.present? - subject + subject = [super] + subject << Gitlab.config.gitlab.email_subject_suffix if Gitlab.config.gitlab.email_subject_suffix.present? + + subject.join(' | ') end end diff --git a/app/mailers/email_rejection_mailer.rb b/app/mailers/email_rejection_mailer.rb index 76db31a4c45..45fc5a6c383 100644 --- a/app/mailers/email_rejection_mailer.rb +++ b/app/mailers/email_rejection_mailer.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class EmailRejectionMailer < BaseMailer def rejection(reason, original_raw, can_retry = false) @reason = reason diff --git a/app/mailers/emails/issues.rb b/app/mailers/emails/issues.rb index 392cc0bee03..c8b1ab5033a 100644 --- a/app/mailers/emails/issues.rb +++ b/app/mailers/emails/issues.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Emails module Issues def new_issue_email(recipient_id, issue_id, reason = nil) diff --git a/app/mailers/emails/members.rb b/app/mailers/emails/members.rb index 75cf56a51f2..91dfdf58982 100644 --- a/app/mailers/emails/members.rb +++ b/app/mailers/emails/members.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Emails module Members extend ActiveSupport::Concern diff --git a/app/mailers/emails/merge_requests.rb b/app/mailers/emails/merge_requests.rb index 70509e9066d..70f65d4e58d 100644 --- a/app/mailers/emails/merge_requests.rb +++ b/app/mailers/emails/merge_requests.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Emails module MergeRequests def new_merge_request_email(recipient_id, merge_request_id, reason = nil) diff --git a/app/mailers/emails/notes.rb b/app/mailers/emails/notes.rb index d9a6fe2a41e..d3284e90568 100644 --- a/app/mailers/emails/notes.rb +++ b/app/mailers/emails/notes.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Emails module Notes def note_commit_email(recipient_id, note_id) diff --git a/app/mailers/emails/pages_domains.rb b/app/mailers/emails/pages_domains.rb index 0027dfdc36b..ce449237ef6 100644 --- a/app/mailers/emails/pages_domains.rb +++ b/app/mailers/emails/pages_domains.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Emails module PagesDomains def pages_domain_enabled_email(domain, recipient) diff --git a/app/mailers/emails/pipelines.rb b/app/mailers/emails/pipelines.rb index f9f45ab987b..31e183640ad 100644 --- a/app/mailers/emails/pipelines.rb +++ b/app/mailers/emails/pipelines.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Emails module Pipelines def pipeline_success_email(pipeline, recipients) @@ -39,10 +41,10 @@ module Emails end def pipeline_subject(status) - commit = @pipeline.short_sha - commit << " in #{@merge_request.to_reference}" if @merge_request + commit = [@pipeline.short_sha] + commit << "in #{@merge_request.to_reference}" if @merge_request - subject("Pipeline ##{@pipeline.id} has #{status} for #{@pipeline.ref}", commit) + subject("Pipeline ##{@pipeline.id} has #{status} for #{@pipeline.ref}", commit.join(' ')) end end end diff --git a/app/mailers/emails/profile.rb b/app/mailers/emails/profile.rb index 4f5edeb9bda..40d7b9ccd7a 100644 --- a/app/mailers/emails/profile.rb +++ b/app/mailers/emails/profile.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Emails module Profile def new_user_email(user_id, token = nil) diff --git a/app/mailers/emails/projects.rb b/app/mailers/emails/projects.rb index 761d873c01c..d7e6c2ba7b2 100644 --- a/app/mailers/emails/projects.rb +++ b/app/mailers/emails/projects.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Emails module Projects def project_was_moved_email(project_id, user_id, old_path_with_namespace) diff --git a/app/mailers/notify.rb b/app/mailers/notify.rb index 0e1e39501f5..f4eeb85270e 100644 --- a/app/mailers/notify.rb +++ b/app/mailers/notify.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Notify < BaseMailer include ActionDispatch::Routing::PolymorphicRoutes include GitlabRoutingHelper @@ -92,12 +94,14 @@ class Notify < BaseMailer # >> subject('Lorem ipsum', 'Dolor sit amet') # => "Lorem ipsum | Dolor sit amet" def subject(*extra) - subject = "" - subject << "#{@project.name} | " if @project - subject << "#{@group.name} | " if @group - subject << extra.join(' | ') if extra.present? - subject << " | #{Gitlab.config.gitlab.email_subject_suffix}" if Gitlab.config.gitlab.email_subject_suffix.present? - subject + subject = [] + + subject << @project.name if @project + subject << @group.name if @group + subject.concat(extra) if extra.present? + subject << Gitlab.config.gitlab.email_subject_suffix if Gitlab.config.gitlab.email_subject_suffix.present? + + subject.join(' | ') end # Return a string suitable for inclusion in the 'Message-Id' mail header. diff --git a/app/mailers/previews/devise_mailer_preview.rb b/app/mailers/previews/devise_mailer_preview.rb index d6588efc486..3b9ef0d3ac0 100644 --- a/app/mailers/previews/devise_mailer_preview.rb +++ b/app/mailers/previews/devise_mailer_preview.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class DeviseMailerPreview < ActionMailer::Preview def confirmation_instructions_for_signup DeviseMailer.confirmation_instructions(unsaved_user, 'faketoken', {}) diff --git a/app/mailers/previews/email_rejection_mailer_preview.rb b/app/mailers/previews/email_rejection_mailer_preview.rb index 639e8471232..402066151ef 100644 --- a/app/mailers/previews/email_rejection_mailer_preview.rb +++ b/app/mailers/previews/email_rejection_mailer_preview.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class EmailRejectionMailerPreview < ActionMailer::Preview def rejection EmailRejectionMailer.rejection("some rejection reason", "From: someone@example.com\nraw email here").message diff --git a/app/mailers/previews/notify_preview.rb b/app/mailers/previews/notify_preview.rb index 3615cde8026..df470930e9e 100644 --- a/app/mailers/previews/notify_preview.rb +++ b/app/mailers/previews/notify_preview.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class NotifyPreview < ActionMailer::Preview def note_merge_request_email_for_individual_note note_email(:note_merge_request_email) do diff --git a/app/mailers/previews/repository_check_mailer_preview.rb b/app/mailers/previews/repository_check_mailer_preview.rb index 19d4eab1805..834d7594719 100644 --- a/app/mailers/previews/repository_check_mailer_preview.rb +++ b/app/mailers/previews/repository_check_mailer_preview.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class RepositoryCheckMailerPreview < ActionMailer::Preview def notify RepositoryCheckMailer.notify(3).message diff --git a/app/mailers/repository_check_mailer.rb b/app/mailers/repository_check_mailer.rb index 22a9f5da646..4bcf371cfc0 100644 --- a/app/mailers/repository_check_mailer.rb +++ b/app/mailers/repository_check_mailer.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class RepositoryCheckMailer < BaseMailer def notify(failed_count) @message = diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index bbe7811841a..c77faa4b71d 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -298,7 +298,8 @@ class ApplicationSetting < ActiveRecord::Base unique_ips_limit_time_window: 3600, usage_ping_enabled: Settings.gitlab['usage_ping_enabled'], instance_statistics_visibility_private: false, - user_default_external: false + user_default_external: false, + user_show_add_ssh_key_message: true } end diff --git a/app/models/award_emoji.rb b/app/models/award_emoji.rb index 99c7866d636..ddc516ccb60 100644 --- a/app/models/award_emoji.rb +++ b/app/models/award_emoji.rb @@ -28,6 +28,23 @@ class AwardEmoji < ActiveRecord::Base .where('name IN (?) AND awardable_type = ? AND awardable_id IN (?)', [DOWNVOTE_NAME, UPVOTE_NAME], type, ids) .group('name', 'awardable_id') end + + # Returns the top 100 emoji awarded by the given user. + # + # The returned value is a Hash mapping emoji names to the number of times + # they were awarded: + # + # { 'thumbsup' => 2, 'thumbsdown' => 1 } + # + # user - The User to get the awards for. + # limt - The maximum number of emoji to return. + def award_counts_for_user(user, limit = 100) + limit(limit) + .where(user: user) + .group(:name) + .order('count_all DESC, name ASC') + .count + end end def downvote? diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 9292929be98..faa160ad6ba 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -67,6 +67,10 @@ module Ci '', Ci::JobArtifact.select(1).where('ci_builds.id = ci_job_artifacts.job_id').archive) end + scope :with_archived_trace, ->() do + where('EXISTS (?)', Ci::JobArtifact.select(1).where('ci_builds.id = ci_job_artifacts.job_id').trace) + end + scope :without_archived_trace, ->() do where('NOT EXISTS (?)', Ci::JobArtifact.select(1).where('ci_builds.id = ci_job_artifacts.job_id').trace) end @@ -77,6 +81,7 @@ module Ci end scope :with_artifacts_stored_locally, -> { with_artifacts_archive.where(artifacts_file_store: [nil, LegacyArtifactUploader::Store::LOCAL]) } + scope :with_archived_trace_stored_locally, -> { with_archived_trace.where(artifacts_file_store: [nil, LegacyArtifactUploader::Store::LOCAL]) } scope :with_artifacts_not_expired, ->() { with_artifacts_archive.where('artifacts_expire_at IS NULL OR artifacts_expire_at > ?', Time.now) } scope :with_expired_artifacts, ->() { with_artifacts_archive.where('artifacts_expire_at < ?', Time.now) } scope :last_month, ->() { where('created_at > ?', Date.today - 1.month) } @@ -226,7 +231,7 @@ module Ci end def cancelable? - active? + active? || created? end def retryable? diff --git a/app/models/ci/job_artifact.rb b/app/models/ci/job_artifact.rb index d7c5f29be96..17b7ee4f07e 100644 --- a/app/models/ci/job_artifact.rb +++ b/app/models/ci/job_artifact.rb @@ -33,7 +33,7 @@ module Ci where(file_type: types) end - delegate :exists?, :open, to: :file + delegate :filename, :exists?, :open, to: :file enum file_type: { archive: 1, diff --git a/app/models/commit.rb b/app/models/commit.rb index 8b9f4490ffa..27fbdc3e386 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -448,6 +448,10 @@ class Commit true end + def to_ability_name + model_name.singular + end + def touch # no-op but needs to be defined since #persisted? is defined end diff --git a/app/models/concerns/awardable.rb b/app/models/concerns/awardable.rb index dd07f389fa5..49981db0d80 100644 --- a/app/models/concerns/awardable.rb +++ b/app/models/concerns/awardable.rb @@ -101,7 +101,7 @@ module Awardable end def remove_award_emoji(name, current_user) - award_emoji.where(name: name, user: current_user).destroy_all + award_emoji.where(name: name, user: current_user).destroy_all # rubocop: disable DestroyAll end def toggle_award_emoji(emoji_name, current_user) diff --git a/app/models/concerns/fast_destroy_all.rb b/app/models/concerns/fast_destroy_all.rb index 65ed46ea202..c342d01243e 100644 --- a/app/models/concerns/fast_destroy_all.rb +++ b/app/models/concerns/fast_destroy_all.rb @@ -34,7 +34,7 @@ module FastDestroyAll included do before_destroy do - raise ForbiddenActionError, '`destroy` and `destroy_all` are forbbiden. Please use `fast_destroy_all`' + raise ForbiddenActionError, '`destroy` and `destroy_all` are forbidden. Please use `fast_destroy_all`' end end diff --git a/app/models/concerns/optionally_search.rb b/app/models/concerns/optionally_search.rb new file mode 100644 index 00000000000..dec97b7dee8 --- /dev/null +++ b/app/models/concerns/optionally_search.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module OptionallySearch + extend ActiveSupport::Concern + + module ClassMethods + def search(*) + raise( + NotImplementedError, + 'Your model must implement the "search" class method' + ) + end + + # Optionally limits a result set to those matching the given search query. + def optionally_search(query = nil) + query.present? ? search(query) : all + end + end +end diff --git a/app/models/internal_id.rb b/app/models/internal_id.rb index 4eb211eff61..e7168d49db9 100644 --- a/app/models/internal_id.rb +++ b/app/models/internal_id.rb @@ -111,7 +111,7 @@ class InternalId < ActiveRecord::Base # Generates next internal id and returns it def generate - subject.transaction do + InternalId.transaction do # Create a record in internal_ids if one does not yet exist # and increment its last value # @@ -125,7 +125,7 @@ class InternalId < ActiveRecord::Base # # Note this will acquire a ROW SHARE lock on the InternalId record def track_greatest(new_value) - subject.transaction do + InternalId.transaction do (lookup || create_record).track_greatest_and_save!(new_value) end end @@ -148,7 +148,7 @@ class InternalId < ActiveRecord::Base # violation. We can safely roll-back the nested transaction and perform # a lookup instead to retrieve the record. def create_record - subject.transaction(requires_new: true) do + InternalId.transaction(requires_new: true) do InternalId.create!( **scope, usage: usage_value, diff --git a/app/models/lfs_object.rb b/app/models/lfs_object.rb index 2a1a4ef48b7..97bf5d611c2 100644 --- a/app/models/lfs_object.rb +++ b/app/models/lfs_object.rb @@ -29,11 +29,13 @@ class LfsObject < ActiveRecord::Base [nil, LfsObjectUploader::Store::LOCAL].include?(self.file_store) end + # rubocop: disable DestroyAll def self.destroy_unreferenced joins("LEFT JOIN lfs_objects_projects ON lfs_objects_projects.lfs_object_id = #{table_name}.id") .where(lfs_objects_projects: { id: nil }) .destroy_all end + # rubocop: enable DestroyAll def self.calculate_oid(path) Digest::SHA256.file(path).hexdigest diff --git a/app/models/license_template.rb b/app/models/license_template.rb new file mode 100644 index 00000000000..0ad75b27827 --- /dev/null +++ b/app/models/license_template.rb @@ -0,0 +1,53 @@ +class LicenseTemplate + PROJECT_TEMPLATE_REGEX = + %r{[\<\{\[] + (project|description| + one\sline\s.+\swhat\sit\sdoes\.) # matching the start and end is enough here + [\>\}\]]}xi.freeze + YEAR_TEMPLATE_REGEX = /[<{\[](year|yyyy)[>}\]]/i.freeze + FULLNAME_TEMPLATE_REGEX = + %r{[\<\{\[] + (fullname|name\sof\s(author|copyright\sowner)) + [\>\}\]]}xi.freeze + + attr_reader :id, :name, :category, :nickname, :url, :meta + + alias_method :key, :id + + def initialize(id:, name:, category:, content:, nickname: nil, url: nil, meta: {}) + @id = id + @name = name + @category = category + @content = content + @nickname = nickname + @url = url + @meta = meta + end + + def popular? + category == :Popular + end + alias_method :featured?, :popular? + + # Returns the text of the license + def content + if @content.respond_to?(:call) + @content = @content.call + else + @content + end + end + + # Populate placeholders in the LicenseTemplate content + def resolve!(project_name: nil, fullname: nil, year: Time.now.year.to_s) + # Ensure the string isn't shared with any other instance of LicenseTemplate + new_content = content.dup + new_content.gsub!(YEAR_TEMPLATE_REGEX, year) if year.present? + new_content.gsub!(PROJECT_TEMPLATE_REGEX, project_name) if project_name.present? + new_content.gsub!(FULLNAME_TEMPLATE_REGEX, fullname) if fullname.present? + + @content = new_content + + self + end +end diff --git a/app/models/namespace.rb b/app/models/namespace.rb index b974309aeb6..0deb44d7916 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -10,6 +10,7 @@ class Namespace < ActiveRecord::Base include Storage::LegacyNamespace include Gitlab::SQL::Pattern include IgnorableColumn + include FeatureGate ignore_column :deleted_at @@ -124,7 +125,6 @@ class Namespace < ActiveRecord::Base def to_param full_path end - alias_method :flipper_id, :to_param def human_name owner_name diff --git a/app/models/notification_setting.rb b/app/models/notification_setting.rb index 1df3a51a7fc..1600acfc575 100644 --- a/app/models/notification_setting.rb +++ b/app/models/notification_setting.rb @@ -45,6 +45,15 @@ class NotificationSetting < ActiveRecord::Base :success_pipeline ].freeze + # Update unfound_translations.rb when events are changed + def self.email_events(source = nil) + EMAIL_EVENTS + end + + def email_events + self.class.email_events(source) + end + EXCLUDED_PARTICIPATING_EVENTS = [ :success_pipeline ].freeze diff --git a/app/models/programming_language.rb b/app/models/programming_language.rb index 400d6c407a7..0e667dac21e 100644 --- a/app/models/programming_language.rb +++ b/app/models/programming_language.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class ProgrammingLanguage < ActiveRecord::Base validates :name, presence: true validates :color, allow_blank: false, color: true diff --git a/app/models/project.rb b/app/models/project.rb index 36089995ed3..8f631d7f0ed 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -27,6 +27,8 @@ class Project < ActiveRecord::Base include FastDestroyAll::Helpers include WithUploads include BatchDestroyDependentAssociations + include FeatureGate + include OptionallySearch extend Gitlab::Cache::RequestCache extend Gitlab::ConfigHelper @@ -139,7 +141,6 @@ class Project < ActiveRecord::Base has_one :flowdock_service has_one :assembla_service has_one :asana_service - has_one :gemnasium_service has_one :mattermost_slash_commands_service has_one :mattermost_service has_one :slack_slash_commands_service @@ -383,6 +384,26 @@ class Project < ActiveRecord::Base only_integer: true, message: 'needs to be beetween 10 minutes and 1 month' } + # Paginates a collection using a `WHERE id < ?` condition. + # + # before - A project ID to use for filtering out projects with an equal or + # greater ID. If no ID is given, all projects are included. + # + # limit - The maximum number of rows to include. + def self.paginate_in_descending_order_using_id( + before: nil, + limit: Kaminari.config.default_per_page + ) + relation = order_id_desc.limit(limit) + relation = relation.where('projects.id < ?', before) if before + + relation + end + + def self.eager_load_namespace_and_owner + includes(namespace: :owner) + end + # Returns a collection of projects that is either public or visible to the # logged in user. def self.public_or_visible_to_user(user = nil) @@ -470,6 +491,24 @@ class Project < ActiveRecord::Base }x end + def reference_postfix + '>' + end + + def reference_postfix_escaped + '>' + end + + # Pattern used to extract `namespace/project>` project references from text. + # '>' or its escaped form ('>') are checked for because '>' is sometimes escaped + # when the reference comes from an external source. + def markdown_reference_pattern + %r{ + #{reference_pattern} + (#{reference_postfix}|#{reference_postfix_escaped}) + }x + end + def trending joins('INNER JOIN trending_projects ON projects.id = trending_projects.project_id') .reorder('trending_projects.id ASC') @@ -501,18 +540,19 @@ class Project < ActiveRecord::Base def auto_devops_enabled? if auto_devops&.enabled.nil? - Gitlab::CurrentSettings.auto_devops_enabled? + has_auto_devops_implicitly_enabled? else auto_devops.enabled? end end def has_auto_devops_implicitly_enabled? - auto_devops&.enabled.nil? && Gitlab::CurrentSettings.auto_devops_enabled? + auto_devops&.enabled.nil? && + (Gitlab::CurrentSettings.auto_devops_enabled? || Feature.enabled?(:force_autodevops_on_by_default, self)) end def has_auto_devops_implicitly_disabled? - auto_devops&.enabled.nil? && !Gitlab::CurrentSettings.auto_devops_enabled? + auto_devops&.enabled.nil? && !(Gitlab::CurrentSettings.auto_devops_enabled? || Feature.enabled?(:force_autodevops_on_by_default, self)) end def empty_repo? @@ -908,6 +948,10 @@ class Project < ActiveRecord::Base end end + def to_reference_with_postfix + "#{to_reference(full: true)}#{self.class.reference_postfix}" + end + # `from` argument can be a Namespace or Project. def to_reference(from = nil, full: false) if full || cross_namespace_reference?(from) diff --git a/app/models/project_auto_devops.rb b/app/models/project_auto_devops.rb index 155400d1a43..dc6736dd9cd 100644 --- a/app/models/project_auto_devops.rb +++ b/app/models/project_auto_devops.rb @@ -47,12 +47,8 @@ class ProjectAutoDevops < ActiveRecord::Base end def needs_to_create_deploy_token? - auto_devops_enabled? && + project.auto_devops_enabled? && !project.public? && !project.deploy_tokens.find_by(name: DeployToken::GITLAB_DEPLOY_TOKEN_NAME).present? end - - def auto_devops_enabled? - Gitlab::CurrentSettings.auto_devops_enabled? || enabled? - end end diff --git a/app/models/project_services/asana_service.rb b/app/models/project_services/asana_service.rb index 4f289e6e215..35c19049c04 100644 --- a/app/models/project_services/asana_service.rb +++ b/app/models/project_services/asana_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'asana' class AsanaService < Service diff --git a/app/models/project_services/assembla_service.rb b/app/models/project_services/assembla_service.rb index 4234b8044e5..60575e45a90 100644 --- a/app/models/project_services/assembla_service.rb +++ b/app/models/project_services/assembla_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class AssemblaService < Service prop_accessor :token, :subdomain validates :token, presence: true, if: :activated? diff --git a/app/models/project_services/bamboo_service.rb b/app/models/project_services/bamboo_service.rb index edc5c00d9c4..d502423726c 100644 --- a/app/models/project_services/bamboo_service.rb +++ b/app/models/project_services/bamboo_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class BambooService < CiService include ReactiveService diff --git a/app/models/project_services/bugzilla_service.rb b/app/models/project_services/bugzilla_service.rb index e4e3a80976b..1a2bb6a171b 100644 --- a/app/models/project_services/bugzilla_service.rb +++ b/app/models/project_services/bugzilla_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class BugzillaService < IssueTrackerService validates :project_url, :issues_url, :new_issue_url, presence: true, public_url: true, if: :activated? diff --git a/app/models/project_services/buildkite_service.rb b/app/models/project_services/buildkite_service.rb index 35884c4560c..43edfde851c 100644 --- a/app/models/project_services/buildkite_service.rb +++ b/app/models/project_services/buildkite_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "addressable/uri" class BuildkiteService < CiService diff --git a/app/models/project_services/builds_email_service.rb b/app/models/project_services/builds_email_service.rb index 0c526b53d72..f2295a95b60 100644 --- a/app/models/project_services/builds_email_service.rb +++ b/app/models/project_services/builds_email_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # This class is to be removed with 9.1 # We should also by then remove BuildsEmailService from database class BuildsEmailService < Service diff --git a/app/models/project_services/campfire_service.rb b/app/models/project_services/campfire_service.rb index cb4af73807b..1d7877a1fb5 100644 --- a/app/models/project_services/campfire_service.rb +++ b/app/models/project_services/campfire_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class CampfireService < Service prop_accessor :token, :subdomain, :room validates :token, presence: true, if: :activated? @@ -82,7 +84,7 @@ class CampfireService < Service before = push[:before] after = push[:after] - message = "" + message = [] message << "[#{project.full_name}] " message << "#{push[:user_name]} " @@ -95,6 +97,6 @@ class CampfireService < Service message << "#{project.web_url}/compare/#{before}...#{after}" end - message + message.join end end diff --git a/app/models/project_services/chat_message/base_message.rb b/app/models/project_services/chat_message/base_message.rb index f710fa85b5d..8c68ddc40f2 100644 --- a/app/models/project_services/chat_message/base_message.rb +++ b/app/models/project_services/chat_message/base_message.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'slack-notifier' module ChatMessage diff --git a/app/models/project_services/chat_message/issue_message.rb b/app/models/project_services/chat_message/issue_message.rb index 3273f41dbd2..0cdcfcf0237 100644 --- a/app/models/project_services/chat_message/issue_message.rb +++ b/app/models/project_services/chat_message/issue_message.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ChatMessage class IssueMessage < BaseMessage attr_reader :title diff --git a/app/models/project_services/chat_message/merge_message.rb b/app/models/project_services/chat_message/merge_message.rb index f412b6833d9..58631e09538 100644 --- a/app/models/project_services/chat_message/merge_message.rb +++ b/app/models/project_services/chat_message/merge_message.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ChatMessage class MergeMessage < BaseMessage attr_reader :merge_request_iid diff --git a/app/models/project_services/chat_message/note_message.rb b/app/models/project_services/chat_message/note_message.rb index 7f9486132e6..741474fb27b 100644 --- a/app/models/project_services/chat_message/note_message.rb +++ b/app/models/project_services/chat_message/note_message.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ChatMessage class NoteMessage < BaseMessage attr_reader :note diff --git a/app/models/project_services/chat_message/pipeline_message.rb b/app/models/project_services/chat_message/pipeline_message.rb index 96fd23aede3..62aec4351db 100644 --- a/app/models/project_services/chat_message/pipeline_message.rb +++ b/app/models/project_services/chat_message/pipeline_message.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ChatMessage class PipelineMessage < BaseMessage attr_reader :ref_type diff --git a/app/models/project_services/chat_message/push_message.rb b/app/models/project_services/chat_message/push_message.rb index 8d599c5f116..82be33a12a1 100644 --- a/app/models/project_services/chat_message/push_message.rb +++ b/app/models/project_services/chat_message/push_message.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ChatMessage class PushMessage < BaseMessage attr_reader :after diff --git a/app/models/project_services/chat_message/wiki_page_message.rb b/app/models/project_services/chat_message/wiki_page_message.rb index d84b80f2de2..b605d289278 100644 --- a/app/models/project_services/chat_message/wiki_page_message.rb +++ b/app/models/project_services/chat_message/wiki_page_message.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ChatMessage class WikiPageMessage < BaseMessage attr_reader :title diff --git a/app/models/project_services/chat_notification_service.rb b/app/models/project_services/chat_notification_service.rb index a60b4c7fd0d..c10ee07ccf4 100644 --- a/app/models/project_services/chat_notification_service.rb +++ b/app/models/project_services/chat_notification_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Base class for Chat notifications services # This class is not meant to be used directly, but only to inherit from. class ChatNotificationService < Service diff --git a/app/models/project_services/ci_service.rb b/app/models/project_services/ci_service.rb index 82979c8bd34..f0ef2d925ab 100644 --- a/app/models/project_services/ci_service.rb +++ b/app/models/project_services/ci_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Base class for CI services # List methods you need to implement to get your CI service # working with GitLab Merge Requests diff --git a/app/models/project_services/custom_issue_tracker_service.rb b/app/models/project_services/custom_issue_tracker_service.rb index 456c7f5cee2..b8f8072869c 100644 --- a/app/models/project_services/custom_issue_tracker_service.rb +++ b/app/models/project_services/custom_issue_tracker_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class CustomIssueTrackerService < IssueTrackerService validates :project_url, :issues_url, :new_issue_url, presence: true, public_url: true, if: :activated? diff --git a/app/models/project_services/deployment_service.rb b/app/models/project_services/deployment_service.rb index 5b8320158fc..6dae4f3a4a6 100644 --- a/app/models/project_services/deployment_service.rb +++ b/app/models/project_services/deployment_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Base class for deployment services # # These services integrate with a deployment solution like Kubernetes/OpenShift, diff --git a/app/models/project_services/drone_ci_service.rb b/app/models/project_services/drone_ci_service.rb index ab4e46da89f..158ae0bf255 100644 --- a/app/models/project_services/drone_ci_service.rb +++ b/app/models/project_services/drone_ci_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class DroneCiService < CiService include ReactiveService diff --git a/app/models/project_services/emails_on_push_service.rb b/app/models/project_services/emails_on_push_service.rb index b604d860a87..fb73d430fb1 100644 --- a/app/models/project_services/emails_on_push_service.rb +++ b/app/models/project_services/emails_on_push_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class EmailsOnPushService < Service boolean_accessor :send_from_committer_email boolean_accessor :disable_diffs diff --git a/app/models/project_services/external_wiki_service.rb b/app/models/project_services/external_wiki_service.rb index a4b1ef09e93..d2835c6ac82 100644 --- a/app/models/project_services/external_wiki_service.rb +++ b/app/models/project_services/external_wiki_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class ExternalWikiService < Service prop_accessor :external_wiki_url diff --git a/app/models/project_services/flowdock_service.rb b/app/models/project_services/flowdock_service.rb index da01ac1b7cf..2545df06f6b 100644 --- a/app/models/project_services/flowdock_service.rb +++ b/app/models/project_services/flowdock_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "flowdock-git-hook" # Flow dock depends on Grit to compute the number of commits between two given diff --git a/app/models/project_services/gemnasium_service.rb b/app/models/project_services/gemnasium_service.rb deleted file mode 100644 index 8a6b0ed1a5f..00000000000 --- a/app/models/project_services/gemnasium_service.rb +++ /dev/null @@ -1,60 +0,0 @@ -require "gemnasium/gitlab_service" - -class GemnasiumService < Service - prop_accessor :token, :api_key - validates :token, :api_key, presence: true, if: :activated? - validate :deprecation_validation - - def title - 'Gemnasium' - end - - def description - 'Gemnasium monitors your project dependencies and alerts you about updates and security vulnerabilities.' - end - - def self.to_param - 'gemnasium' - end - - def fields - [ - { type: 'text', name: 'api_key', placeholder: 'Your personal API KEY on gemnasium.com ', required: true }, - { type: 'text', name: 'token', placeholder: 'The project\'s slug on gemnasium.com', required: true } - ] - end - - def self.supported_events - %w(push) - end - - def deprecated? - true - end - - def deprecation_message - "Gemnasium has been acquired by GitLab in January 2018. Since May 15, 2018, the service provided by Gemnasium is no longer available." - end - - def deprecation_validation - errors[:base] << deprecation_message - end - - def execute(data) - return unless supported_events.include?(data[:object_kind]) - - # Gitaly: this class will be removed https://gitlab.com/gitlab-org/gitlab-ee/issues/6010 - repo_path = Gitlab::GitalyClient::StorageSettings.allow_disk_access do - project.repository.path_to_repo - end - - Gemnasium::GitlabService.execute( - ref: data[:ref], - before: data[:before], - after: data[:after], - token: token, - api_key: api_key, - repo: repo_path - ) - end -end diff --git a/app/models/project_services/gitlab_issue_tracker_service.rb b/app/models/project_services/gitlab_issue_tracker_service.rb index 16e32a4139e..fa9abf58e62 100644 --- a/app/models/project_services/gitlab_issue_tracker_service.rb +++ b/app/models/project_services/gitlab_issue_tracker_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class GitlabIssueTrackerService < IssueTrackerService include Gitlab::Routing diff --git a/app/models/project_services/hangouts_chat_service.rb b/app/models/project_services/hangouts_chat_service.rb index a8512c5f57c..272cd0f4e47 100644 --- a/app/models/project_services/hangouts_chat_service.rb +++ b/app/models/project_services/hangouts_chat_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'hangouts_chat' class HangoutsChatService < ChatNotificationService diff --git a/app/models/project_services/hipchat_service.rb b/app/models/project_services/hipchat_service.rb index dce878e485f..66012f0da99 100644 --- a/app/models/project_services/hipchat_service.rb +++ b/app/models/project_services/hipchat_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class HipchatService < Service include ActionView::Helpers::SanitizeHelper @@ -108,7 +110,7 @@ class HipchatService < Service before = push[:before] after = push[:after] - message = "" + message = [] message << "#{push[:user_name]} " if Gitlab::Git.blank_ref?(before) @@ -132,7 +134,7 @@ class HipchatService < Service end end - message + message.join end def markdown(text, options = {}) @@ -165,11 +167,11 @@ class HipchatService < Service description = obj_attr[:description] issue_link = "<a href=\"#{issue_url}\">issue ##{issue_iid}</a>" - message = "#{user_name} #{state} #{issue_link} in #{project_link}: <b>#{title}</b>" + message = ["#{user_name} #{state} #{issue_link} in #{project_link}: <b>#{title}</b>"] message << "<pre>#{markdown(description)}</pre>" - message + message.join end def create_merge_request_message(data) @@ -184,12 +186,11 @@ class HipchatService < Service merge_request_url = "#{project_url}/merge_requests/#{merge_request_id}" merge_request_link = "<a href=\"#{merge_request_url}\">merge request !#{merge_request_id}</a>" - message = "#{user_name} #{state} #{merge_request_link} in " \ - "#{project_link}: <b>#{title}</b>" + message = ["#{user_name} #{state} #{merge_request_link} in " \ + "#{project_link}: <b>#{title}</b>"] message << "<pre>#{markdown(description)}</pre>" - - message + message.join end def format_title(title) @@ -235,12 +236,11 @@ class HipchatService < Service end subject_html = "<a href=\"#{note_url}\">#{subject_type} #{subject_desc}</a>" - message = "#{user_name} commented on #{subject_html} in #{project_link}: " + message = ["#{user_name} commented on #{subject_html} in #{project_link}: "] message << title message << "<pre>#{markdown(note, ref: commit_id)}</pre>" - - message + message.join end def create_pipeline_message(data) diff --git a/app/models/project_services/irker_service.rb b/app/models/project_services/irker_service.rb index 27bdf708c80..a783a314071 100644 --- a/app/models/project_services/irker_service.rb +++ b/app/models/project_services/irker_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'uri' class IrkerService < Service diff --git a/app/models/project_services/issue_tracker_service.rb b/app/models/project_services/issue_tracker_service.rb index df6dcd90985..c7520d766a8 100644 --- a/app/models/project_services/issue_tracker_service.rb +++ b/app/models/project_services/issue_tracker_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class IssueTrackerService < Service validate :one_issue_tracker, if: :activated?, on: :manual_change diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb index 82d438d5378..cc98b3f5a41 100644 --- a/app/models/project_services/jira_service.rb +++ b/app/models/project_services/jira_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class JiraService < IssueTrackerService include Gitlab::Routing include ApplicationHelper diff --git a/app/models/project_services/kubernetes_service.rb b/app/models/project_services/kubernetes_service.rb index 722642f6da7..bda1f67b8ff 100644 --- a/app/models/project_services/kubernetes_service.rb +++ b/app/models/project_services/kubernetes_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + ## # NOTE: # We'll move this class to Clusters::Platforms::Kubernetes, which contains exactly the same logic. diff --git a/app/models/project_services/mattermost_service.rb b/app/models/project_services/mattermost_service.rb index 0362ed172c7..b8bc83b870e 100644 --- a/app/models/project_services/mattermost_service.rb +++ b/app/models/project_services/mattermost_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class MattermostService < ChatNotificationService def title 'Mattermost notifications' diff --git a/app/models/project_services/mattermost_slash_commands_service.rb b/app/models/project_services/mattermost_slash_commands_service.rb index 227d430083d..ca324f68d2d 100644 --- a/app/models/project_services/mattermost_slash_commands_service.rb +++ b/app/models/project_services/mattermost_slash_commands_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class MattermostSlashCommandsService < SlashCommandsService include TriggersHelper diff --git a/app/models/project_services/microsoft_teams_service.rb b/app/models/project_services/microsoft_teams_service.rb index 99500caec0e..5b0e5fed092 100644 --- a/app/models/project_services/microsoft_teams_service.rb +++ b/app/models/project_services/microsoft_teams_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class MicrosoftTeamsService < ChatNotificationService def title 'Microsoft Teams Notification' diff --git a/app/models/project_services/mock_ci_service.rb b/app/models/project_services/mock_ci_service.rb index b89dc07a73e..6883976f0c8 100644 --- a/app/models/project_services/mock_ci_service.rb +++ b/app/models/project_services/mock_ci_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # For an example companion mocking service, see https://gitlab.com/gitlab-org/gitlab-mock-ci-service class MockCiService < CiService ALLOWED_STATES = %w[failed canceled running pending success success_with_warnings skipped not_found].freeze diff --git a/app/models/project_services/mock_deployment_service.rb b/app/models/project_services/mock_deployment_service.rb index 59a3811ce5d..7ab1687f8ba 100644 --- a/app/models/project_services/mock_deployment_service.rb +++ b/app/models/project_services/mock_deployment_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class MockDeploymentService < DeploymentService def title 'Mock deployment' diff --git a/app/models/project_services/mock_monitoring_service.rb b/app/models/project_services/mock_monitoring_service.rb index ed0318c6b27..bcf8f1df5da 100644 --- a/app/models/project_services/mock_monitoring_service.rb +++ b/app/models/project_services/mock_monitoring_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class MockMonitoringService < MonitoringService def title 'Mock monitoring' diff --git a/app/models/project_services/monitoring_service.rb b/app/models/project_services/monitoring_service.rb index 9af68b4e821..1b530a8247b 100644 --- a/app/models/project_services/monitoring_service.rb +++ b/app/models/project_services/monitoring_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Base class for monitoring services # # These services integrate with a deployment solution like Prometheus diff --git a/app/models/project_services/packagist_service.rb b/app/models/project_services/packagist_service.rb index ba62a5b7ac0..003884bb7ac 100644 --- a/app/models/project_services/packagist_service.rb +++ b/app/models/project_services/packagist_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class PackagistService < Service prop_accessor :username, :token, :server diff --git a/app/models/project_services/pipelines_email_service.rb b/app/models/project_services/pipelines_email_service.rb index 4cf149ac044..6f39a5e6e83 100644 --- a/app/models/project_services/pipelines_email_service.rb +++ b/app/models/project_services/pipelines_email_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class PipelinesEmailService < Service prop_accessor :recipients boolean_accessor :notify_only_broken_pipelines diff --git a/app/models/project_services/pivotaltracker_service.rb b/app/models/project_services/pivotaltracker_service.rb index 3476e7d2283..617e502b639 100644 --- a/app/models/project_services/pivotaltracker_service.rb +++ b/app/models/project_services/pivotaltracker_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class PivotaltrackerService < Service API_ENDPOINT = 'https://www.pivotaltracker.com/services/v5/source_commits'.freeze diff --git a/app/models/project_services/prometheus_service.rb b/app/models/project_services/prometheus_service.rb index df4254e0523..509e5b6089b 100644 --- a/app/models/project_services/prometheus_service.rb +++ b/app/models/project_services/prometheus_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class PrometheusService < MonitoringService include PrometheusAdapter diff --git a/app/models/project_services/pushover_service.rb b/app/models/project_services/pushover_service.rb index 8777a44b72f..4e48c348b45 100644 --- a/app/models/project_services/pushover_service.rb +++ b/app/models/project_services/pushover_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class PushoverService < Service BASE_URI = 'https://api.pushover.net/1'.freeze @@ -79,7 +81,7 @@ class PushoverService < Service end if data[:total_commits_count] > 0 - message << "\nTotal commits count: #{data[:total_commits_count]}" + message = [message, "Total commits count: #{data[:total_commits_count]}"].join("\n") end pushover_data = { diff --git a/app/models/project_services/redmine_service.rb b/app/models/project_services/redmine_service.rb index 3721093a6d1..a80be4b06da 100644 --- a/app/models/project_services/redmine_service.rb +++ b/app/models/project_services/redmine_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class RedmineService < IssueTrackerService validates :project_url, :issues_url, :new_issue_url, presence: true, public_url: true, if: :activated? diff --git a/app/models/project_services/slack_service.rb b/app/models/project_services/slack_service.rb index 71da0af75f6..482808255f9 100644 --- a/app/models/project_services/slack_service.rb +++ b/app/models/project_services/slack_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class SlackService < ChatNotificationService def title 'Slack notifications' diff --git a/app/models/project_services/slack_slash_commands_service.rb b/app/models/project_services/slack_slash_commands_service.rb index 1c3892a3f75..6c82e088231 100644 --- a/app/models/project_services/slack_slash_commands_service.rb +++ b/app/models/project_services/slack_slash_commands_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class SlackSlashCommandsService < SlashCommandsService include TriggersHelper diff --git a/app/models/project_services/slash_commands_service.rb b/app/models/project_services/slash_commands_service.rb index 37ea45109ae..e3ab60adefd 100644 --- a/app/models/project_services/slash_commands_service.rb +++ b/app/models/project_services/slash_commands_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Base class for Chat services # This class is not meant to be used directly, but only to inherrit from. class SlashCommandsService < Service diff --git a/app/models/project_services/teamcity_service.rb b/app/models/project_services/teamcity_service.rb index 802678147cf..eeeff5e802a 100644 --- a/app/models/project_services/teamcity_service.rb +++ b/app/models/project_services/teamcity_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class TeamcityService < CiService include ReactiveService diff --git a/app/models/protected_branch/merge_access_level.rb b/app/models/protected_branch/merge_access_level.rb index e8d35ac326f..b0d5c64e931 100644 --- a/app/models/protected_branch/merge_access_level.rb +++ b/app/models/protected_branch/merge_access_level.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class ProtectedBranch::MergeAccessLevel < ActiveRecord::Base include ProtectedBranchAccess end diff --git a/app/models/protected_branch/push_access_level.rb b/app/models/protected_branch/push_access_level.rb index 7a2e9e5ec5d..b2a88229853 100644 --- a/app/models/protected_branch/push_access_level.rb +++ b/app/models/protected_branch/push_access_level.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class ProtectedBranch::PushAccessLevel < ActiveRecord::Base include ProtectedBranchAccess end diff --git a/app/models/protected_tag/create_access_level.rb b/app/models/protected_tag/create_access_level.rb index 6b6ab3d8279..b06e55fb5dd 100644 --- a/app/models/protected_tag/create_access_level.rb +++ b/app/models/protected_tag/create_access_level.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class ProtectedTag::CreateAccessLevel < ActiveRecord::Base include ProtectedTagAccess diff --git a/app/models/repository_language.rb b/app/models/repository_language.rb index f467d4eafa3..b18142a2ac4 100644 --- a/app/models/repository_language.rb +++ b/app/models/repository_language.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class RepositoryLanguage < ActiveRecord::Base belongs_to :project belongs_to :programming_language diff --git a/app/models/site_statistic.rb b/app/models/site_statistic.rb index 9c9c3172fe6..48324570f0b 100644 --- a/app/models/site_statistic.rb +++ b/app/models/site_statistic.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class SiteStatistic < ActiveRecord::Base # prevents the creation of multiple rows default_value_for :id, 1 @@ -47,7 +49,7 @@ class SiteStatistic < ActiveRecord::Base # # @return [SiteStatistic] record with tracked information def self.fetch - SiteStatistic.transaction(requires_new: true) do + transaction(requires_new: true) do SiteStatistic.first_or_create! end rescue ActiveRecord::RecordNotUnique diff --git a/app/models/storage/hashed_project.rb b/app/models/storage/hashed_project.rb index 26b4b78ac64..90710f73fd3 100644 --- a/app/models/storage/hashed_project.rb +++ b/app/models/storage/hashed_project.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Storage class HashedProject attr_accessor :project diff --git a/app/models/storage/legacy_project.rb b/app/models/storage/legacy_project.rb index 27cb388c702..9f6f19acb41 100644 --- a/app/models/storage/legacy_project.rb +++ b/app/models/storage/legacy_project.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Storage class LegacyProject attr_accessor :project diff --git a/app/models/system_note_metadata.rb b/app/models/system_note_metadata.rb index c5c77bc8333..376ef673ca8 100644 --- a/app/models/system_note_metadata.rb +++ b/app/models/system_note_metadata.rb @@ -15,7 +15,7 @@ class SystemNoteMetadata < ActiveRecord::Base commit description merge confidential visible label assignee cross_reference title time_tracking branch milestone discussion task moved opened closed merged duplicate locked unlocked - outdated + outdated tag ].freeze validates :note, presence: true diff --git a/app/models/user.rb b/app/models/user.rb index 37f2e8b680e..a6ba90794d6 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -19,6 +19,7 @@ class User < ActiveRecord::Base include BulkMemberAccessLoad include BlocksJsonSerialization include WithUploads + include OptionallySearch DEFAULT_NOTIFICATION_LEVEL = :participating @@ -101,6 +102,10 @@ class User < ActiveRecord::Base has_many :groups, through: :group_members has_many :owned_groups, -> { where(members: { access_level: Gitlab::Access::OWNER }) }, through: :group_members, source: :group has_many :maintainers_groups, -> { where(members: { access_level: Gitlab::Access::MAINTAINER }) }, through: :group_members, source: :group + has_many :owned_or_maintainers_groups, + -> { where(members: { access_level: [Gitlab::Access::MAINTAINER, Gitlab::Access::OWNER] }) }, + through: :group_members, + source: :group alias_attribute :masters_groups, :maintainers_groups # Projects @@ -249,11 +254,41 @@ class User < ActiveRecord::Base scope :external, -> { where(external: true) } scope :active, -> { with_state(:active).non_internal } scope :without_projects, -> { joins('LEFT JOIN project_authorizations ON users.id = project_authorizations.user_id').where(project_authorizations: { user_id: nil }) } - scope :todo_authors, ->(user_id, state) { where(id: Todo.where(user_id: user_id, state: state).select(:author_id)) } scope :order_recent_sign_in, -> { reorder(Gitlab::Database.nulls_last_order('current_sign_in_at', 'DESC')) } scope :order_oldest_sign_in, -> { reorder(Gitlab::Database.nulls_last_order('current_sign_in_at', 'ASC')) } scope :confirmed, -> { where.not(confirmed_at: nil) } + # Limits the users to those that have TODOs, optionally in the given state. + # + # user - The user to get the todos for. + # + # with_todos - If we should limit the result set to users that are the + # authors of todos. + # + # todo_state - An optional state to require the todos to be in. + def self.limit_to_todo_authors(user: nil, with_todos: false, todo_state: nil) + if user && with_todos + where(id: Todo.where(user: user, state: todo_state).select(:author_id)) + else + all + end + end + + # Returns a relation that optionally includes the given user. + # + # user_id - The ID of the user to include. + def self.union_with_user(user_id = nil) + if user_id.present? + union = Gitlab::SQL::Union.new([all, User.unscoped.where(id: user_id)]) + + # We use "unscoped" here so that any inner conditions are not repeated for + # the outer query, which would be redundant. + User.unscoped.from("(#{union.to_sql}) #{User.table_name}") + else + all + end + end + def self.with_two_factor_indistinct joins("LEFT OUTER JOIN u2f_registrations AS u2f ON u2f.user_id = users.id") .where("u2f.id IS NOT NULL OR users.otp_required_for_login = ?", true) @@ -361,6 +396,18 @@ class User < ActiveRecord::Base ).reorder(order % { query: ActiveRecord::Base.connection.quote(query) }, :name) end + # Limits the result set to users _not_ in the given query/list of IDs. + # + # users - The list of users to ignore. This can be an + # `ActiveRecord::Relation`, or an Array. + def where_not_in(users = nil) + users ? where.not(id: users) : all + end + + def reorder_by_name + reorder(:name) + end + # searches user by given pattern # it compares name, email, username fields and user's secondary emails with given pattern # This method uses ILIKE on PostgreSQL and LIKE on MySQL. @@ -512,7 +559,7 @@ class User < ActiveRecord::Base otp_grace_period_started_at: nil, otp_backup_codes: nil ) - self.u2f_registrations.destroy_all + self.u2f_registrations.destroy_all # rubocop: disable DestroyAll end end @@ -982,15 +1029,7 @@ class User < ActiveRecord::Base end def manageable_groups - union_sql = Gitlab::SQL::Union.new([owned_groups.select(:id), maintainers_groups.select(:id)]).to_sql - - # Update this line to not use raw SQL when migrated to Rails 5.2. - # Either ActiveRecord or Arel constructions are fine. - # This was replaced with the raw SQL construction because of bugs in the arel gem. - # Bugs were fixed in arel 9.0.0 (Rails 5.2). - owned_and_maintainer_groups = Group.where("namespaces.id IN (#{union_sql})") # rubocop:disable GitlabSecurity/SqlInjection - - Gitlab::GroupHierarchy.new(owned_and_maintainer_groups).base_and_descendants + Gitlab::GroupHierarchy.new(owned_or_maintainers_groups).base_and_descendants end def namespaces @@ -1244,11 +1283,6 @@ class User < ActiveRecord::Base !terms_accepted? end - def owned_or_maintainers_groups - union = Gitlab::SQL::Union.new([owned_groups, maintainers_groups]) - Group.from("(#{union.to_sql}) namespaces") - end - # @deprecated alias_method :owned_or_masters_groups, :owned_or_maintainers_groups diff --git a/app/policies/commit_policy.rb b/app/policies/commit_policy.rb new file mode 100644 index 00000000000..67e9bc12804 --- /dev/null +++ b/app/policies/commit_policy.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class CommitPolicy < BasePolicy + delegate { @subject.project } +end diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb index f52a3bad77d..00c58f15013 100644 --- a/app/policies/project_policy.rb +++ b/app/policies/project_policy.rb @@ -251,6 +251,7 @@ class ProjectPolicy < BasePolicy enable :update_pages enable :read_cluster enable :create_cluster + enable :create_environment_terminal end rule { (mirror_available & can?(:admin_project)) | admin }.enable :admin_remote_mirror diff --git a/app/presenters/ci/build_runner_presenter.rb b/app/presenters/ci/build_runner_presenter.rb index 02f6c5bdf81..880218e2727 100644 --- a/app/presenters/ci/build_runner_presenter.rb +++ b/app/presenters/ci/build_runner_presenter.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Ci class BuildRunnerPresenter < SimpleDelegator def artifacts diff --git a/app/serializers/environment_entity.rb b/app/serializers/environment_entity.rb index b18e9706db6..07a13c33b89 100644 --- a/app/serializers/environment_entity.rb +++ b/app/serializers/environment_entity.rb @@ -23,9 +23,8 @@ class EnvironmentEntity < Grape::Entity stop_project_environment_path(environment.project, environment) end - expose :terminal_path, if: ->(environment, _) { environment.has_terminals? } do |environment| - can?(request.current_user, :admin_environment, environment.project) && - terminal_project_environment_path(environment.project, environment) + expose :terminal_path, if: ->(*) { environment.has_terminals? && can_access_terminal? } do |environment| + terminal_project_environment_path(environment.project, environment) end expose :folder_path do |environment| @@ -40,7 +39,13 @@ class EnvironmentEntity < Grape::Entity private + alias_method :environment, :object + def current_user request.current_user end + + def can_access_terminal? + can?(request.current_user, :create_environment_terminal, environment) + end end diff --git a/app/serializers/move_to_project_entity.rb b/app/serializers/move_to_project_entity.rb new file mode 100644 index 00000000000..dac1124b0b3 --- /dev/null +++ b/app/serializers/move_to_project_entity.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +class MoveToProjectEntity < Grape::Entity + expose :id + expose :name_with_namespace +end diff --git a/app/serializers/move_to_project_serializer.rb b/app/serializers/move_to_project_serializer.rb new file mode 100644 index 00000000000..6a59317505c --- /dev/null +++ b/app/serializers/move_to_project_serializer.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class MoveToProjectSerializer < BaseSerializer + entity MoveToProjectEntity +end diff --git a/app/serializers/project_mirror_serializer.rb b/app/serializers/project_mirror_serializer.rb new file mode 100644 index 00000000000..6a9462aa7cb --- /dev/null +++ b/app/serializers/project_mirror_serializer.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class ProjectMirrorSerializer < BaseSerializer + entity ProjectMirrorEntity +end diff --git a/app/serializers/test_case_entity.rb b/app/serializers/test_case_entity.rb index 5c1cbf37182..ec60055ba5b 100644 --- a/app/serializers/test_case_entity.rb +++ b/app/serializers/test_case_entity.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class TestCaseEntity < Grape::Entity expose :status expose :name diff --git a/app/serializers/test_reports_comparer_entity.rb b/app/serializers/test_reports_comparer_entity.rb index b95d820d093..d7a3dd34fdc 100644 --- a/app/serializers/test_reports_comparer_entity.rb +++ b/app/serializers/test_reports_comparer_entity.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class TestReportsComparerEntity < Grape::Entity expose :total_status, as: :status diff --git a/app/serializers/test_reports_comparer_serializer.rb b/app/serializers/test_reports_comparer_serializer.rb index a739858efb2..7fb8d28b09a 100644 --- a/app/serializers/test_reports_comparer_serializer.rb +++ b/app/serializers/test_reports_comparer_serializer.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class TestReportsComparerSerializer < BaseSerializer entity TestReportsComparerEntity end diff --git a/app/serializers/test_suite_comparer_entity.rb b/app/serializers/test_suite_comparer_entity.rb index a3965ba3930..9fa3a897ebe 100644 --- a/app/serializers/test_suite_comparer_entity.rb +++ b/app/serializers/test_suite_comparer_entity.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class TestSuiteComparerEntity < Grape::Entity expose :name expose :total_status, as: :status diff --git a/app/services/ci/enqueue_build_service.rb b/app/services/ci/enqueue_build_service.rb new file mode 100644 index 00000000000..8140651d980 --- /dev/null +++ b/app/services/ci/enqueue_build_service.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true +module Ci + class EnqueueBuildService < BaseService + def execute(build) + build.enqueue + end + end +end diff --git a/app/services/ci/process_pipeline_service.rb b/app/services/ci/process_pipeline_service.rb index cda9bbff3b4..cafee76a33c 100644 --- a/app/services/ci/process_pipeline_service.rb +++ b/app/services/ci/process_pipeline_service.rb @@ -37,7 +37,7 @@ module Ci def process_build(build, current_status) if valid_statuses_for_when(build.when).include?(current_status) - build.action? ? build.actionize : build.enqueue + build.action? ? build.actionize : enqueue_build(build) true else build.skip @@ -93,5 +93,9 @@ module Ci .where.not(id: latest_statuses.map(&:first)) .update_all(retried: true) if latest_statuses.any? end + + def enqueue_build(build) + Ci::EnqueueBuildService.new(project, @user).execute(build) + end end end diff --git a/app/services/commits/tag_service.rb b/app/services/commits/tag_service.rb new file mode 100644 index 00000000000..7961ba4d3c4 --- /dev/null +++ b/app/services/commits/tag_service.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +module Commits + class TagService < BaseService + def execute(commit) + unless params[:tag_name] + return error('Missing parameter tag_name') + end + + tag_name = params[:tag_name] + message = params[:tag_message] + release_description = nil + + result = Tags::CreateService + .new(commit.project, current_user) + .execute(tag_name, commit.sha, message, release_description) + + if result[:status] == :success + tag = result[:tag] + SystemNoteService.tag_commit(commit, commit.project, current_user, tag.name) + end + + result + end + end +end diff --git a/app/services/groups/destroy_service.rb b/app/services/groups/destroy_service.rb index c4554ce45fb..12aeba4af71 100644 --- a/app/services/groups/destroy_service.rb +++ b/app/services/groups/destroy_service.rb @@ -2,6 +2,8 @@ module Groups class DestroyService < Groups::BaseService + DestroyError = Class.new(StandardError) + def async_execute job_id = GroupDestroyWorker.perform_async(group.id, current_user.id) Rails.logger.info("User #{current_user.id} scheduled a deletion of group ID #{group.id} with job ID #{job_id}") @@ -12,9 +14,8 @@ module Groups group.projects.each do |project| # Execute the destruction of the models immediately to ensure atomic cleanup. - # Skip repository removal because we remove directory with namespace - # that contain all these repositories - ::Projects::DestroyService.new(project, current_user, skip_repo: project.legacy_storage?).execute + success = ::Projects::DestroyService.new(project, current_user).execute + raise DestroyError, "Project #{project.id} can't be deleted" unless success end group.children.each do |group| diff --git a/app/services/labels/promote_service.rb b/app/services/labels/promote_service.rb index c0463052821..623a5f0950e 100644 --- a/app/services/labels/promote_service.rb +++ b/app/services/labels/promote_service.rb @@ -65,7 +65,7 @@ module Labels end def update_project_labels(label_ids) - Label.where(id: label_ids).destroy_all + Label.where(id: label_ids).destroy_all # rubocop: disable DestroyAll end def clone_label_to_group_label(label) diff --git a/app/services/milestones/promote_service.rb b/app/services/milestones/promote_service.rb index 37aa6d3a9bc..660b4faaec0 100644 --- a/app/services/milestones/promote_service.rb +++ b/app/services/milestones/promote_service.rb @@ -73,7 +73,7 @@ module Milestones end def destroy_old_milestones(milestone) - Milestone.where(id: milestone_ids_for_merge(milestone)).destroy_all + Milestone.where(id: milestone_ids_for_merge(milestone)).destroy_all # rubocop: disable DestroyAll end def group_project_ids diff --git a/app/services/notes/quick_actions_service.rb b/app/services/notes/quick_actions_service.rb index 7280449bb1c..4c14d834949 100644 --- a/app/services/notes/quick_actions_service.rb +++ b/app/services/notes/quick_actions_service.rb @@ -4,7 +4,8 @@ module Notes class QuickActionsService < BaseService UPDATE_SERVICES = { 'Issue' => Issues::UpdateService, - 'MergeRequest' => MergeRequests::UpdateService + 'MergeRequest' => MergeRequests::UpdateService, + 'Commit' => Commits::TagService }.freeze def self.noteable_update_service(note) diff --git a/app/services/notification_recipient_service.rb b/app/services/notification_recipient_service.rb index 4389fd89538..5c0e8a35cb0 100644 --- a/app/services/notification_recipient_service.rb +++ b/app/services/notification_recipient_service.rb @@ -130,7 +130,7 @@ module NotificationRecipientService end def add_project_watchers - add_recipients(project_watchers, :watch, nil) + add_recipients(project_watchers, :watch, nil) if project end def add_group_watchers @@ -220,6 +220,8 @@ module NotificationRecipientService end class Default < Base + MENTION_TYPE_ACTIONS = [:new_issue, :new_merge_request].freeze + attr_reader :target attr_reader :current_user attr_reader :action @@ -252,7 +254,7 @@ module NotificationRecipientService add_subscribed_users - if [:new_issue, :new_merge_request].include?(custom_action) + if self.class.mention_type_actions.include?(custom_action) # These will all be participants as well, but adding with the :mention # type ensures that users with the mention notification level will # receive them, too. @@ -279,10 +281,14 @@ module NotificationRecipientService end # Build event key to search on custom notification level - # Check NotificationSetting::EMAIL_EVENTS + # Check NotificationSetting.email_events def custom_action @custom_action ||= "#{action}_#{target.class.model_name.name.underscore}".to_sym end + + def self.mention_type_actions + MENTION_TYPE_ACTIONS.dup + end end class NewNote < Base diff --git a/app/services/preview_markdown_service.rb b/app/services/preview_markdown_service.rb index a15ee4911ef..11b996ed4b6 100644 --- a/app/services/preview_markdown_service.rb +++ b/app/services/preview_markdown_service.rb @@ -16,7 +16,7 @@ class PreviewMarkdownService < BaseService private def explain_quick_actions(text) - return text, [] unless %w(Issue MergeRequest).include?(commands_target_type) + return text, [] unless %w(Issue MergeRequest Commit).include?(commands_target_type) quick_actions_service = QuickActions::InterpretService.new(project, current_user) quick_actions_service.explain(text, find_commands_target) @@ -29,13 +29,9 @@ class PreviewMarkdownService < BaseService end def find_commands_target - if commands_target_id.present? - finder = commands_target_type == 'Issue' ? IssuesFinder : MergeRequestsFinder - finder.new(current_user, project_id: project.id).find(commands_target_id) - else - collection = commands_target_type == 'Issue' ? project.issues : project.merge_requests - collection.build - end + QuickActions::TargetService + .new(project, current_user) + .execute(commands_target_type, commands_target_id) end def commands_target_type diff --git a/app/services/projects/autocomplete_service.rb b/app/services/projects/autocomplete_service.rb index 10eb2cea4a2..5286b92ab6b 100644 --- a/app/services/projects/autocomplete_service.rb +++ b/app/services/projects/autocomplete_service.rb @@ -47,15 +47,7 @@ module Projects end def commands(noteable, type) - noteable ||= - case type - when 'Issue' - @project.issues.build - when 'MergeRequest' - @project.merge_requests.build - end - - return [] unless noteable&.is_a?(Issuable) + return [] unless noteable QuickActions::InterpretService.new(project, current_user).available_commands(noteable) end diff --git a/app/services/projects/destroy_service.rb b/app/services/projects/destroy_service.rb index 46a8a5e4d98..76e22507698 100644 --- a/app/services/projects/destroy_service.rb +++ b/app/services/projects/destroy_service.rb @@ -83,9 +83,6 @@ module Projects end def remove_repository(path) - # Skip repository removal. We use this flag when remove user or group - return true if params[:skip_repo] == true - # There is a possibility project does not have repository or wiki return true unless repo_exists?(path) diff --git a/app/services/projects/detect_repository_languages_service.rb b/app/services/projects/detect_repository_languages_service.rb index 4b4108de231..3488b9ce47e 100644 --- a/app/services/projects/detect_repository_languages_service.rb +++ b/app/services/projects/detect_repository_languages_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Projects class DetectRepositoryLanguagesService < BaseService attr_reader :detected_repository_languages, :programming_languages diff --git a/app/services/projects/move_deploy_keys_projects_service.rb b/app/services/projects/move_deploy_keys_projects_service.rb index 40a22837eaf..9f3f44f30ea 100644 --- a/app/services/projects/move_deploy_keys_projects_service.rb +++ b/app/services/projects/move_deploy_keys_projects_service.rb @@ -27,7 +27,7 @@ module Projects end def remove_remaining_deploy_keys_projects - source_project.deploy_keys_projects.destroy_all + source_project.deploy_keys_projects.destroy_all # rubocop: disable DestroyAll end end end diff --git a/app/services/projects/move_lfs_objects_projects_service.rb b/app/services/projects/move_lfs_objects_projects_service.rb index a5099519594..f78546a1e9c 100644 --- a/app/services/projects/move_lfs_objects_projects_service.rb +++ b/app/services/projects/move_lfs_objects_projects_service.rb @@ -21,7 +21,7 @@ module Projects end def remove_remaining_lfs_objects_project - source_project.lfs_objects_projects.destroy_all + source_project.lfs_objects_projects.destroy_all # rubocop: disable DestroyAll end def non_existent_lfs_objects_projects diff --git a/app/services/projects/move_notification_settings_service.rb b/app/services/projects/move_notification_settings_service.rb index 746605d56f1..109a00dd6d9 100644 --- a/app/services/projects/move_notification_settings_service.rb +++ b/app/services/projects/move_notification_settings_service.rb @@ -22,7 +22,7 @@ module Projects # Remove remaining notification settings from source_project def remove_remaining_notification_settings - source_project.notification_settings.destroy_all + source_project.notification_settings.destroy_all # rubocop: disable DestroyAll end # Get users of current notification_settings diff --git a/app/services/projects/move_project_group_links_service.rb b/app/services/projects/move_project_group_links_service.rb index d9038030f7e..1efafdce36d 100644 --- a/app/services/projects/move_project_group_links_service.rb +++ b/app/services/projects/move_project_group_links_service.rb @@ -26,7 +26,7 @@ module Projects # Remove remaining project group links from source_project def remove_remaining_project_group_links - source_project.reload.project_group_links.destroy_all + source_project.reload.project_group_links.destroy_all # rubocop: disable DestroyAll end def group_links_in_target_project diff --git a/app/services/projects/move_project_members_service.rb b/app/services/projects/move_project_members_service.rb index bb0c0d10242..ec983582d94 100644 --- a/app/services/projects/move_project_members_service.rb +++ b/app/services/projects/move_project_members_service.rb @@ -25,7 +25,7 @@ module Projects def remove_remaining_members # Remove remaining members and authorizations from source_project - source_project.project_members.destroy_all + source_project.project_members.destroy_all # rubocop: disable DestroyAll end def project_members_in_target_project diff --git a/app/services/projects/update_service.rb b/app/services/projects/update_service.rb index 97f181ccea8..e390d7a04c3 100644 --- a/app/services/projects/update_service.rb +++ b/app/services/projects/update_service.rb @@ -30,7 +30,7 @@ module Projects def run_auto_devops_pipeline? return false if project.repository.gitlab_ci_yml || !project.auto_devops&.previous_changes&.include?('enabled') - project.auto_devops.enabled? || (project.auto_devops.enabled.nil? && Gitlab::CurrentSettings.auto_devops_enabled?) + project.auto_devops_enabled? end private diff --git a/app/services/protected_branches/legacy_api_update_service.rb b/app/services/protected_branches/legacy_api_update_service.rb index 1f6bbe72f85..da8bf2ce02a 100644 --- a/app/services/protected_branches/legacy_api_update_service.rb +++ b/app/services/protected_branches/legacy_api_update_service.rb @@ -38,11 +38,11 @@ module ProtectedBranches def delete_redundant_access_levels unless @developers_can_merge.nil? - @protected_branch.merge_access_levels.destroy_all + @protected_branch.merge_access_levels.destroy_all # rubocop: disable DestroyAll end unless @developers_can_push.nil? - @protected_branch.push_access_levels.destroy_all + @protected_branch.push_access_levels.destroy_all # rubocop: disable DestroyAll end end end diff --git a/app/services/quick_actions/interpret_service.rb b/app/services/quick_actions/interpret_service.rb index cdc8514c47c..8838ed06324 100644 --- a/app/services/quick_actions/interpret_service.rb +++ b/app/services/quick_actions/interpret_service.rb @@ -60,7 +60,8 @@ module QuickActions "Closes this #{issuable.to_ability_name.humanize(capitalize: false)}." end condition do - issuable.persisted? && + issuable.is_a?(Issuable) && + issuable.persisted? && issuable.open? && current_user.can?(:"update_#{issuable.to_ability_name}", issuable) end @@ -75,7 +76,8 @@ module QuickActions "Reopens this #{issuable.to_ability_name.humanize(capitalize: false)}." end condition do - issuable.persisted? && + issuable.is_a?(Issuable) && + issuable.persisted? && issuable.closed? && current_user.can?(:"update_#{issuable.to_ability_name}", issuable) end @@ -149,7 +151,8 @@ module QuickActions issuable.allows_multiple_assignees? ? '@user1 @user2' : '' end condition do - issuable.persisted? && + issuable.is_a?(Issuable) && + issuable.persisted? && issuable.assignees.any? && current_user.can?(:"admin_#{issuable.to_ability_name}", project) end @@ -188,7 +191,8 @@ module QuickActions "Removes #{issuable.milestone.to_reference(format: :name)} milestone." end condition do - issuable.persisted? && + issuable.is_a?(Issuable) && + issuable.persisted? && issuable.milestone_id? && current_user.can?(:"admin_#{issuable.to_ability_name}", project) end @@ -231,7 +235,8 @@ module QuickActions end params '~label1 ~"label 2"' condition do - issuable.persisted? && + issuable.is_a?(Issuable) && + issuable.persisted? && issuable.labels.any? && current_user.can?(:"admin_#{issuable.to_ability_name}", project) end @@ -257,7 +262,8 @@ module QuickActions end params '~label1 ~"label 2"' condition do - issuable.persisted? && + issuable.is_a?(Issuable) && + issuable.persisted? && issuable.labels.any? && current_user.can?(:"admin_#{issuable.to_ability_name}", project) end @@ -295,7 +301,8 @@ module QuickActions desc 'Add a todo' explanation 'Adds a todo.' condition do - issuable.persisted? && + issuable.is_a?(Issuable) && + issuable.persisted? && !TodoService.new.todo_exist?(issuable, current_user) end command :todo do @@ -317,7 +324,8 @@ module QuickActions "Subscribes to this #{issuable.to_ability_name.humanize(capitalize: false)}." end condition do - issuable.persisted? && + issuable.is_a?(Issuable) && + issuable.persisted? && !issuable.subscribed?(current_user, project) end command :subscribe do @@ -329,7 +337,8 @@ module QuickActions "Unsubscribes from this #{issuable.to_ability_name.humanize(capitalize: false)}." end condition do - issuable.persisted? && + issuable.is_a?(Issuable) && + issuable.persisted? && issuable.subscribed?(current_user, project) end command :unsubscribe do @@ -385,7 +394,8 @@ module QuickActions end params ':emoji:' condition do - issuable.persisted? + issuable.is_a?(Issuable) && + issuable.persisted? end parse_params do |emoji_param| match = emoji_param.match(Banzai::Filter::EmojiFilter.emoji_pattern) @@ -574,6 +584,23 @@ module QuickActions @updates[:confidential] = true end + desc 'Tag this commit.' + explanation do |tag_name, message| + with_message = %{ with "#{message}"} if message.present? + "Tags this commit to #{tag_name}#{with_message}." + end + params 'v1.2.3 <message>' + parse_params do |tag_name_and_message| + tag_name_and_message.split(' ', 2) + end + condition do + issuable.is_a?(Commit) && current_user.can?(:push_code, project) + end + command :tag do |tag_name, message| + @updates[:tag_name] = tag_name + @updates[:tag_message] = message + end + def extract_users(params) return [] if params.nil? diff --git a/app/services/quick_actions/target_service.rb b/app/services/quick_actions/target_service.rb new file mode 100644 index 00000000000..d8ba52c6e50 --- /dev/null +++ b/app/services/quick_actions/target_service.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +module QuickActions + class TargetService < BaseService + def execute(type, type_id) + case type&.downcase + when 'issue' + issue(type_id) + when 'mergerequest' + merge_request(type_id) + when 'commit' + commit(type_id) + end + end + + private + + def issue(type_id) + IssuesFinder.new(current_user, project_id: project.id).find_by(iid: type_id) || project.issues.build + end + + def merge_request(type_id) + MergeRequestsFinder.new(current_user, project_id: project.id).find_by(iid: type_id) || project.merge_requests.build + end + + def commit(type_id) + project.commit(type_id) + end + end +end diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb index 77494295f14..dda89830179 100644 --- a/app/services/system_note_service.rb +++ b/app/services/system_note_service.rb @@ -32,6 +32,21 @@ module SystemNoteService create_note(NoteSummary.new(noteable, project, author, body, action: 'commit', commit_count: total_count)) end + # Called when a commit was tagged + # + # noteable - Noteable object + # project - Project owning noteable + # author - User performing the tag + # tag_name - The created tag name + # + # Returns the created Note object + def tag_commit(noteable, project, author, tag_name) + link = url_helpers.project_tag_url(project, id: tag_name) + body = "tagged commit #{noteable.sha} to [`#{tag_name}`](#{link})" + + create_note(NoteSummary.new(noteable, project, author, body, action: 'tag')) + end + # Called when the assignee of a Noteable is changed or removed # # noteable - Noteable object diff --git a/app/services/tags/create_service.rb b/app/services/tags/create_service.rb index 329722df747..35390f5082c 100644 --- a/app/services/tags/create_service.rb +++ b/app/services/tags/create_service.rb @@ -7,7 +7,7 @@ module Tags return error('Tag name invalid') unless valid_tag repository = project.repository - message&.strip! + message = message&.strip new_tag = nil diff --git a/app/services/todos/destroy/base_service.rb b/app/services/todos/destroy/base_service.rb index dff5e1f30e5..aeb60e50c64 100644 --- a/app/services/todos/destroy/base_service.rb +++ b/app/services/todos/destroy/base_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Todos module Destroy class BaseService diff --git a/app/services/todos/destroy/confidential_issue_service.rb b/app/services/todos/destroy/confidential_issue_service.rb index c5b66df057a..efec0f22da5 100644 --- a/app/services/todos/destroy/confidential_issue_service.rb +++ b/app/services/todos/destroy/confidential_issue_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Todos module Destroy class ConfidentialIssueService < ::Todos::Destroy::BaseService diff --git a/app/services/todos/destroy/entity_leave_service.rb b/app/services/todos/destroy/entity_leave_service.rb index 045f5ecaae7..4cb9d08713d 100644 --- a/app/services/todos/destroy/entity_leave_service.rb +++ b/app/services/todos/destroy/entity_leave_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Todos module Destroy class EntityLeaveService < ::Todos::Destroy::BaseService diff --git a/app/services/todos/destroy/group_private_service.rb b/app/services/todos/destroy/group_private_service.rb index d13fa7a6516..f67f1d40597 100644 --- a/app/services/todos/destroy/group_private_service.rb +++ b/app/services/todos/destroy/group_private_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Todos module Destroy class GroupPrivateService < ::Todos::Destroy::BaseService diff --git a/app/services/todos/destroy/private_features_service.rb b/app/services/todos/destroy/private_features_service.rb index 4d8e2877bfb..7e204885b31 100644 --- a/app/services/todos/destroy/private_features_service.rb +++ b/app/services/todos/destroy/private_features_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Todos module Destroy class PrivateFeaturesService < ::Todos::Destroy::BaseService diff --git a/app/services/todos/destroy/project_private_service.rb b/app/services/todos/destroy/project_private_service.rb index 315a0c33398..ae8fab3ffca 100644 --- a/app/services/todos/destroy/project_private_service.rb +++ b/app/services/todos/destroy/project_private_service.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Todos module Destroy class ProjectPrivateService < ::Todos::Destroy::BaseService diff --git a/app/services/users/destroy_service.rb b/app/services/users/destroy_service.rb index 4bc78b5b64e..73fa6089945 100644 --- a/app/services/users/destroy_service.rb +++ b/app/services/users/destroy_service.rb @@ -2,6 +2,8 @@ module Users class DestroyService + DestroyError = Class.new(StandardError) + attr_accessor :current_user def initialize(current_user) @@ -46,9 +48,8 @@ module Users namespace.prepare_for_destroy user.personal_projects.each do |project| - # Skip repository removal because we remove directory with namespace - # that contain all this repositories - ::Projects::DestroyService.new(project, current_user, skip_repo: project.legacy_storage?).execute + success = ::Projects::DestroyService.new(project, current_user).execute + raise DestroyError, "Project #{project.id} can't be deleted" unless success end yield(user) if block_given? diff --git a/app/views/admin/application_settings/_account_and_limit.html.haml b/app/views/admin/application_settings/_account_and_limit.html.haml index 7c8243a7a90..622cb11010e 100644 --- a/app/views/admin/application_settings/_account_and_limit.html.haml +++ b/app/views/admin/application_settings/_account_and_limit.html.haml @@ -29,5 +29,11 @@ = f.check_box :user_default_external, class: 'form-check-input' = f.label :user_default_external, class: 'form-check-label' do Newly registered users will by default be external + .form-group + = f.label :user_show_add_ssh_key_message, 'Prompt users to upload SSH keys', class: 'label-bold' + .form-check + = f.check_box :user_show_add_ssh_key_message, class: 'form-check-input' + = f.label :user_show_add_ssh_key_message, class: 'form-check-label' do + Inform users without uploaded SSH keys that they can't push over SSH until one is added = f.submit 'Save changes', class: 'btn btn-success' diff --git a/app/views/admin/application_settings/_ci_cd.html.haml b/app/views/admin/application_settings/_ci_cd.html.haml index 5037017e38a..97be658cd34 100644 --- a/app/views/admin/application_settings/_ci_cd.html.haml +++ b/app/views/admin/application_settings/_ci_cd.html.haml @@ -38,6 +38,8 @@ .form-text.text-muted Set the default expiration time for each job's artifacts. 0 for unlimited. + The default unit is in seconds, but you can define an alternative. For example: + <code>4 mins 2 sec</code>, <code>2h42min</code>. = link_to icon('question-circle'), help_page_path('user/admin_area/settings/continuous_integration', anchor: 'default-artifacts-expiration') = f.submit 'Save changes', class: "btn btn-success" diff --git a/app/views/admin/application_settings/show.html.haml b/app/views/admin/application_settings/show.html.haml index 258d50ad676..6133a7646f4 100644 --- a/app/views/admin/application_settings/show.html.haml +++ b/app/views/admin/application_settings/show.html.haml @@ -325,6 +325,8 @@ .settings-content = render partial: 'repository_mirrors_form' += render_if_exists 'admin/application_settings/templates', expanded: expanded + %section.settings.as-third-party-offers.no-animate#js-third-party-offers-settings{ class: ('expanded' if expanded) } .settings-header %h4 diff --git a/app/views/admin/runners/index.html.haml b/app/views/admin/runners/index.html.haml index 8dfd176f1b7..9280ff4d478 100644 --- a/app/views/admin/runners/index.html.haml +++ b/app/views/admin/runners/index.html.haml @@ -49,7 +49,7 @@ = submit_tag 'Search', class: 'btn' .float-right.light - Runners with last contact more than a minute ago: #{@active_runners_cnt} + Runners currently online: #{@active_runners_cnt} %br diff --git a/app/views/admin/spam_logs/index.html.haml b/app/views/admin/spam_logs/index.html.haml index 8aaa6379730..b45d3e4823b 100644 --- a/app/views/admin/spam_logs/index.html.haml +++ b/app/views/admin/spam_logs/index.html.haml @@ -17,6 +17,6 @@ %th Primary Action %th = render @spam_logs - = paginate @spam_logs + = paginate @spam_logs, theme: 'gitlab' - else %h4 There are no Spam Logs diff --git a/app/views/admin/users/_access_levels.html.haml b/app/views/admin/users/_access_levels.html.haml index 04acc5f8423..5f68163163e 100644 --- a/app/views/admin/users/_access_levels.html.haml +++ b/app/views/admin/users/_access_levels.html.haml @@ -1,15 +1,18 @@ %fieldset %legend Access .form-group.row - = f.label :projects_limit, class: 'col-form-label col-sm-2' + .col-sm-2.text-right + = f.label :projects_limit, class: 'col-form-label' .col-sm-10= f.number_field :projects_limit, min: 0, max: Gitlab::Database::MAX_INT_VALUE, class: 'form-control' .form-group.row - = f.label :can_create_group, class: 'col-form-label col-sm-2' + .col-sm-2.text-right + = f.label :can_create_group, class: 'col-form-label' .col-sm-10= f.check_box :can_create_group .form-group.row - = f.label :access_level, class: 'col-form-label col-sm-2' + .col-sm-2.text-right + = f.label :access_level, class: 'col-form-label' .col-sm-10 - editing_current_user = (current_user == @user) @@ -29,7 +32,8 @@ You cannot remove your own admin rights. .form-group.row - = f.label :external, class: 'col-form-label col-sm-2' + .col-sm-2.text-right + = f.label :external, class: 'col-form-label' .col-sm-10 = f.check_box :external do External diff --git a/app/views/admin/users/_form.html.haml b/app/views/admin/users/_form.html.haml index 58be07fc83e..7f21bdb91c8 100644 --- a/app/views/admin/users/_form.html.haml +++ b/app/views/admin/users/_form.html.haml @@ -5,17 +5,20 @@ %fieldset %legend Account .form-group.row - = f.label :name, class: 'col-form-label col-sm-2' + .col-sm-2.text-right + = f.label :name, class: 'col-form-label' .col-sm-10 = f.text_field :name, required: true, autocomplete: 'off', class: 'form-control' %span.help-inline * required .form-group.row - = f.label :username, class: 'col-form-label col-sm-2' + .col-sm-2.text-right + = f.label :username, class: 'col-form-label' .col-sm-10 = f.text_field :username, required: true, autocomplete: 'off', autocorrect: 'off', autocapitalize: 'off', spellcheck: false, class: 'form-control' %span.help-inline * required .form-group.row - = f.label :email, class: 'col-form-label col-sm-2' + .col-sm-2.text-right + = f.label :email, class: 'col-form-label' .col-sm-10 = f.text_field :email, required: true, autocomplete: 'off', class: 'form-control' %span.help-inline * required @@ -24,7 +27,8 @@ %fieldset %legend Password .form-group.row - = f.label :password, class: 'col-form-label col-sm-2' + .col-sm-2.text-right + = f.label :password, class: 'col-form-label' .col-sm-10 %strong Reset link will be generated and sent to the user. @@ -34,10 +38,12 @@ %fieldset %legend Password .form-group.row - = f.label :password, class: 'col-form-label col-sm-2' + .col-sm-2.text-right + = f.label :password, class: 'col-form-label' .col-sm-10= f.password_field :password, disabled: f.object.force_random_password, class: 'form-control' .form-group.row - = f.label :password_confirmation, class: 'col-form-label col-sm-2' + .col-sm-2.text-right + = f.label :password_confirmation, class: 'col-form-label' .col-sm-10= f.password_field :password_confirmation, disabled: f.object.force_random_password, class: 'form-control' = render partial: 'access_levels', locals: { f: f } @@ -45,21 +51,26 @@ %fieldset %legend Profile .form-group.row - = f.label :avatar, class: 'col-form-label col-sm-2' + .col-sm-2.text-right + = f.label :avatar, class: 'col-form-label' .col-sm-10 = f.file_field :avatar .form-group.row - = f.label :skype, class: 'col-form-label col-sm-2' + .col-sm-2.text-right + = f.label :skype, class: 'col-form-label' .col-sm-10= f.text_field :skype, class: 'form-control' .form-group.row - = f.label :linkedin, class: 'col-form-label col-sm-2' + .col-sm-2.text-right + = f.label :linkedin, class: 'col-form-label' .col-sm-10= f.text_field :linkedin, class: 'form-control' .form-group.row - = f.label :twitter, class: 'col-form-label col-sm-2' + .col-sm-2.text-right + = f.label :twitter, class: 'col-form-label' .col-sm-10= f.text_field :twitter, class: 'form-control' .form-group.row - = f.label :website_url, 'Website', class: 'col-form-label col-sm-2' + .col-sm-2.text-right + = f.label :website_url, 'Website', class: 'col-form-label' .col-sm-10= f.text_field :website_url, class: 'form-control' .form-actions diff --git a/app/views/admin/users/show.html.haml b/app/views/admin/users/show.html.haml index f730fd05176..029efadd75d 100644 --- a/app/views/admin/users/show.html.haml +++ b/app/views/admin/users/show.html.haml @@ -128,8 +128,8 @@ .col-md-6 - unless @user == current_user - unless @user.confirmed? - .card.bg-info - .card-header + .card.border-info + .card-header.bg-info.text-white Confirm user .card-body - if @user.unconfirmed_email.present? @@ -138,8 +138,8 @@ %br = link_to 'Confirm user', confirm_admin_user_path(@user), method: :put, class: "btn btn-info", data: { confirm: 'Are you sure?' } - if @user.blocked? - .card.bg-info - .card-header + .card.border-info + .card-header.bg-info.text-white This user is blocked .card-body %p A blocked user cannot: @@ -162,8 +162,8 @@ %br = link_to 'Block user', block_admin_user_path(@user), data: { confirm: 'USER WILL BE BLOCKED! Are you sure?' }, method: :put, class: "btn btn-warning" - if @user.access_locked? - .card.bg-info - .card-header + .card.border-info + .card-header.bg-info.text-white This account has been locked .card-body %p This user has been temporarily locked due to excessive number of failed logins. You may manually unlock the account. diff --git a/app/views/instance_statistics/cohorts/_cohorts_table.html.haml b/app/views/instance_statistics/cohorts/_cohorts_table.html.haml index 701a4e62b39..6a7c999bff3 100644 --- a/app/views/instance_statistics/cohorts/_cohorts_table.html.haml +++ b/app/views/instance_statistics/cohorts/_cohorts_table.html.haml @@ -3,7 +3,7 @@ User cohorts are shown for the last #{@cohorts[:months_included]} months. Only users with activity are counted in the cohort total; inactive users are counted separately. - = link_to icon('question-circle'), help_page_path('user/admin_area/user_cohorts', anchor: 'cohorts'), title: 'About this feature', target: '_blank' + = link_to icon('question-circle'), help_page_path('user/instance_statistics/user_cohorts', anchor: 'cohorts'), title: 'About this feature', target: '_blank' .table-holder %table.table diff --git a/app/views/instance_statistics/conversational_development_index/_no_data.html.haml b/app/views/instance_statistics/conversational_development_index/_no_data.html.haml index d69c46194b4..dd795aee135 100644 --- a/app/views/instance_statistics/conversational_development_index/_no_data.html.haml +++ b/app/views/instance_statistics/conversational_development_index/_no_data.html.haml @@ -4,4 +4,4 @@ %h4 Data is still calculating... %p In order to gather accurate feature usage data, it can take 1 to 2 weeks to see your index. - = link_to 'Learn more', help_page_path('user/admin_area/monitoring/convdev'), target: '_blank' + = link_to 'Learn more', help_page_path('user/instance_statistics/convdev'), target: '_blank' diff --git a/app/views/instance_statistics/conversational_development_index/index.html.haml b/app/views/instance_statistics/conversational_development_index/index.html.haml index e3d1aa31dc2..dd63b98376f 100644 --- a/app/views/instance_statistics/conversational_development_index/index.html.haml +++ b/app/views/instance_statistics/conversational_development_index/index.html.haml @@ -19,7 +19,7 @@ index %br score - = link_to icon('question-circle', 'aria-hidden' => 'true'), help_page_path('user/admin_area/monitoring/convdev') + = link_to icon('question-circle', 'aria-hidden' => 'true'), help_page_path('user/instance_statistics/convdev') .convdev-cards.board-card-container - @metric.cards.each do |card| diff --git a/app/views/projects/blob/_editor.html.haml b/app/views/projects/blob/_editor.html.haml index 8560b72fe85..24f256d083b 100644 --- a/app/views/projects/blob/_editor.html.haml +++ b/app/views/projects/blob/_editor.html.haml @@ -2,11 +2,11 @@ .file-holder-bottom-radius.file-holder.file.append-bottom-default .js-file-title.file-title.clearfix{ data: { current_action: action } } - .editor-ref + .editor-ref.block-truncated = sprite_icon('fork', size: 12) = ref - %span.editor-file-name - - if current_action?(:edit) || current_action?(:update) + - if current_action?(:edit) || current_action?(:update) + %span.editor-file-name = text_field_tag 'file_path', (params[:file_path] || @path), class: 'form-control new-file-path js-file-path-name-input' @@ -16,7 +16,7 @@ = text_field_tag 'file_name', params[:file_name], placeholder: "File name", required: true, class: 'form-control new-file-name js-file-path-name-input' - .float-right.file-buttons + .file-buttons = button_tag class: 'soft-wrap-toggle btn', type: 'button', tabindex: '-1' do %span.no-wrap = custom_icon('icon_no_wrap') diff --git a/app/views/projects/environments/metrics.html.haml b/app/views/projects/environments/metrics.html.haml index af86b8e8e67..4222963a754 100644 --- a/app/views/projects/environments/metrics.html.haml +++ b/app/views/projects/environments/metrics.html.haml @@ -2,11 +2,4 @@ - page_title "Metrics for environment", @environment.name .prometheus-container{ class: container_class } - .top-area - .row - .col-sm-6 - %h3 - Environment: - = link_to @environment.name, environment_path(@environment) - #prometheus-graphs{ data: metrics_data(@project, @environment) } diff --git a/app/views/projects/forks/_fork_button.html.haml b/app/views/projects/forks/_fork_button.html.haml index 8a549d431ee..12cf40bb65f 100644 --- a/app/views/projects/forks/_fork_button.html.haml +++ b/app/views/projects/forks/_fork_button.html.haml @@ -5,7 +5,7 @@ .bordered-box.fork-thumbnail.text-center.prepend-left-default.append-right-default.prepend-top-default.append-bottom-default.forked = link_to project_path(forked_project) do - if /no_((\w*)_)*avatar/.match(avatar) - = project_identicon(namespace, class: "avatar s100 identicon") + = project_icon(namespace, class: "avatar s100 identicon") - else .avatar-container.s100 = image_tag(avatar, class: "avatar s100") @@ -18,7 +18,7 @@ class: ("disabled has-tooltip" unless can_create_project), title: (_('You have reached your project limit') unless can_create_project) do - if /no_((\w*)_)*avatar/.match(avatar) - = project_identicon(namespace, class: "avatar s100 identicon") + = project_icon(namespace, class: "avatar s100 identicon") - else .avatar-container.s100 = image_tag(avatar, class: "avatar s100") diff --git a/app/views/projects/jobs/_sidebar.html.haml b/app/views/projects/jobs/_sidebar.html.haml index 759efd4e9d4..86b2b8bf2f7 100644 --- a/app/views/projects/jobs/_sidebar.html.haml +++ b/app/views/projects/jobs/_sidebar.html.haml @@ -1,12 +1,7 @@ %aside.right-sidebar.right-sidebar-expanded.build-sidebar.js-build-sidebar.js-right-sidebar{ data: { "offset-top" => "101", "spy" => "affix" } } .sidebar-container .blocks-container - - if can?(current_user, :create_build_terminal, @build) - .block - = link_to terminal_project_job_path(@project, @build), class: 'terminal-button pull-right btn visible-md-block visible-lg-block', title: 'Terminal' do - Terminal - - #js-details-block-vue{ data: { can_user_retry: can?(current_user, :update_build, @build) && @build.retryable? } } + #js-details-block-vue{ data: { terminal_path: can?(current_user, :create_build_terminal, @build) && @build.has_terminal? ? terminal_project_job_path(@project, @build) : nil } } - if can?(current_user, :read_build, @project) && (@build.artifacts? || @build.artifacts_expired?) .block diff --git a/app/views/projects/mirrors/_instructions.html.haml b/app/views/projects/mirrors/_instructions.html.haml index 64f0fde30cf..e051f9e6331 100644 --- a/app/views/projects/mirrors/_instructions.html.haml +++ b/app/views/projects/mirrors/_instructions.html.haml @@ -1,10 +1,11 @@ .account-well.prepend-top-default.append-bottom-default %ul %li - The repository must be accessible over <code>http://</code>, <code>https://</code>, <code>ssh://</code> or <code>git://</code>. - %li - Include the username in the URL if required: <code>https://username@gitlab.company.com/group/project.git</code>. - %li - The update action will time out after 10 minutes. For big repositories, use a clone/push combination. - %li - The Git LFS objects will <strong>not</strong> be synced. + = _('The repository must be accessible over <code>http://</code>, + <code>https://</code>, <code>ssh://</code> and <code>git://</code>.').html_safe + %li= _('Include the username in the URL if required: <code>https://username@gitlab.company.com/group/project.git</code>.').html_safe + %li= _("The update action will time out after #{import_will_timeout_message(Gitlab.config.gitlab_shell.git_timeout)} minutes. For big repositories, use a clone/push combination.") + %li= _('The Git LFS objects will <strong>not</strong> be synced.').html_safe + %li + = _('This user will be the author of all events in the activity feed that are the result of an update, + like new branches being created or new commits being pushed to existing branches.') diff --git a/app/views/projects/mirrors/_mirror_repos.html.haml b/app/views/projects/mirrors/_mirror_repos.html.haml new file mode 100644 index 00000000000..c6764c7607a --- /dev/null +++ b/app/views/projects/mirrors/_mirror_repos.html.haml @@ -0,0 +1,63 @@ +- expanded = Rails.env.test? +- protocols = Gitlab::UrlSanitizer::ALLOWED_SCHEMES.join('|') + +%section.settings.project-mirror-settings.js-mirror-settings.no-animate#js-push-remote-settings{ class: ('expanded' if expanded) } + .settings-header + %h4= _('Mirroring repositories') + %button.btn.js-settings-toggle + = expanded ? _('Collapse') : _('Expand') + %p + = _('Set up your project to automatically push and/or pull changes to/from another repository. Branches, tags, and commits will be synced automatically.') + = link_to _('Read more'), help_page_path('workflow/repository_mirroring'), target: '_blank' + + .settings-content + = form_for @project, url: project_mirror_path(@project), html: { class: 'gl-show-field-errors js-mirror-form', autocomplete: 'false', data: mirrors_form_data_attributes } do |f| + .panel.panel-default + .panel-heading + %h3.panel-title= _('Mirror a repository') + .panel-body + %div= form_errors(@project) + + .form-group.has-feedback + = label_tag :url, _('Git repository URL'), class: 'label-light' + = text_field_tag :url, nil, class: 'form-control js-mirror-url js-repo-url', placeholder: _('Input your repository URL'), required: true, pattern: "(#{protocols}):\/\/.+" + + = render 'projects/mirrors/instructions' + + = render 'projects/mirrors/mirror_repos_form', f: f + + .form-check.append-bottom-10 + = check_box_tag :only_protected_branches, '1', false, class: 'js-mirror-protected form-check-input' + = label_tag :only_protected_branches, _('Only mirror protected branches'), class: 'form-check-label' + = link_to icon('question-circle'), help_page_path('user/project/protected_branches') + + .panel-footer + = f.submit _('Mirror repository'), class: 'btn btn-create', name: :update_remote_mirror + + .panel.panel-default + .table-responsive + %table.table.push-pull-table + %thead + %tr + %th + = _('Mirrored repositories') + = render_if_exists 'projects/mirrors/mirrored_repositories_count' + %th= _('Direction') + %th= _('Last update') + %th + %th + %tbody.js-mirrors-table-body + = render_if_exists 'projects/mirrors/table_pull_row' + - @project.remote_mirrors.each_with_index do |mirror, index| + - if mirror.enabled + %tr + %td= mirror.safe_url + %td= _('Push') + %td= mirror.last_update_at.present? ? time_ago_with_tooltip(mirror.last_update_at) : _('Never') + %td + - if mirror.last_error.present? + .badge.mirror-error-badge{ data: { toggle: 'tooltip', html: 'true' }, title: html_escape(mirror.last_error.try(:strip)) }= _('Error') + %td.mirror-action-buttons + .btn-group.mirror-actions-group.pull-right{ role: 'group' } + = render 'shared/remote_mirror_update_button', remote_mirror: mirror + %button.js-delete-mirror.btn.btn-danger{ type: 'button', data: { mirror_id: mirror.id, toggle: 'tooltip', container: 'body' }, title: _('Remove') }= icon('trash-o') diff --git a/app/views/projects/mirrors/_mirror_repos_form.html.haml b/app/views/projects/mirrors/_mirror_repos_form.html.haml new file mode 100644 index 00000000000..93994cb30ac --- /dev/null +++ b/app/views/projects/mirrors/_mirror_repos_form.html.haml @@ -0,0 +1,18 @@ +- protocols = Gitlab::UrlSanitizer::ALLOWED_SCHEMES.join('|') + +.form-group + = label_tag :mirror_direction, _('Mirror direction'), class: 'label-light' + = select_tag :mirror_direction, options_for_select([[_('Push'), 'push']]), class: 'form-control js-mirror-direction', disabled: true + += f.fields_for :remote_mirrors, @project.remote_mirrors.build do |rm_f| + = rm_f.hidden_field :enabled, value: '1' + = rm_f.hidden_field :url, class: 'js-mirror-url-hidden', required: true, pattern: "(#{protocols}):\/\/.+" + = rm_f.hidden_field :only_protected_branches, class: 'js-mirror-protected-hidden' + +.form-group + = label_tag :auth_method, _('Authentication method'), class: 'label-bold' + = select_tag :auth_method, options_for_select([[_('None'), 'none'], [_('Password'), 'password']], 'none'), { class: "form-control js-auth-method" } + +.form-group.js-password-group.collapse + = label_tag :password, _('Password'), class: 'label-bold' + = text_field_tag :password, '', class: 'form-control js-password' diff --git a/app/views/projects/mirrors/_push.html.haml b/app/views/projects/mirrors/_push.html.haml deleted file mode 100644 index 08375e09816..00000000000 --- a/app/views/projects/mirrors/_push.html.haml +++ /dev/null @@ -1,50 +0,0 @@ -- expanded = Rails.env.test? -%section.settings.no-animate#js-push-remote-settings{ class: ('expanded' if expanded) } - .settings-header - %h4 - Push to a remote repository - %button.btn.js-settings-toggle - = expanded ? 'Collapse' : 'Expand' - %p - Set up the remote repository that you want to update with the content of the current repository - every time someone pushes to it. - = link_to 'Read more', help_page_path('workflow/repository_mirroring', anchor: 'pushing-to-a-remote-repository'), target: '_blank' - .settings-content - = form_for @project, url: project_mirror_path(@project) do |f| - %div - = form_errors(@project) - = render "shared/remote_mirror_update_button", remote_mirror: @remote_mirror - - if @remote_mirror.last_error.present? - .panel.panel-danger - .panel-heading - - if @remote_mirror.last_update_at - The remote repository failed to update #{time_ago_with_tooltip(@remote_mirror.last_update_at)}. - - else - The remote repository failed to update. - - - if @remote_mirror.last_successful_update_at - Last successful update #{time_ago_with_tooltip(@remote_mirror.last_successful_update_at)}. - .panel-body - %pre - :preserve - #{h(@remote_mirror.last_error.strip)} - = f.fields_for :remote_mirrors, @remote_mirror do |rm_form| - .form-group - = rm_form.check_box :enabled, class: "float-left" - .prepend-left-20 - = rm_form.label :enabled, "Remote mirror repository", class: "label-bold append-bottom-0" - %p.light.append-bottom-0 - Automatically update the remote mirror's branches, tags, and commits from this repository every time someone pushes to it. - .form-group.has-feedback - = rm_form.label :url, "Git repository URL", class: "label-bold" - = rm_form.text_field :url, class: "form-control", placeholder: 'https://username:password@gitlab.company.com/group/project.git' - - = render "projects/mirrors/instructions" - - .form-group - = rm_form.check_box :only_protected_branches, class: 'float-left' - .prepend-left-20 - = rm_form.label :only_protected_branches, class: 'label-bold' - = link_to icon('question-circle'), help_page_path('user/project/protected_branches') - - = f.submit 'Save changes', class: 'btn btn-create', name: 'update_remote_mirror' diff --git a/app/views/projects/mirrors/_show.html.haml b/app/views/projects/mirrors/_show.html.haml index de77701a373..8318d5898a1 100644 --- a/app/views/projects/mirrors/_show.html.haml +++ b/app/views/projects/mirrors/_show.html.haml @@ -1,3 +1 @@ -- if can?(current_user, :admin_remote_mirror, @project) - = render 'projects/mirrors/push' - += render 'projects/mirrors/mirror_repos' diff --git a/app/views/projects/pages/_use.html.haml b/app/views/projects/pages/_use.html.haml index cd9177c0f9e..988dabef3a0 100644 --- a/app/views/projects/pages/_use.html.haml +++ b/app/views/projects/pages/_use.html.haml @@ -1,6 +1,6 @@ - unless @project.pages_deployed? - .card.bg-info - .card-header + .card.border-info + .card-header.bg-info.text-white Configure pages .card-body %p diff --git a/app/views/projects/settings/ci_cd/_form.html.haml b/app/views/projects/settings/ci_cd/_form.html.haml index 434aed2f603..9134257b631 100644 --- a/app/views/projects/settings/ci_cd/_form.html.haml +++ b/app/views/projects/settings/ci_cd/_form.html.haml @@ -17,7 +17,7 @@ %h5.prepend-top-0 = _("Git strategy for pipelines") %p - = _("Choose between <code>clone</code> or <code>fetch</code> to get the recent application code") + = _("Choose between <code>clone</code> or <code>fetch</code> to get the recent application code").html_safe = link_to icon('question-circle'), help_page_path('user/project/pipelines/settings', anchor: 'git-strategy'), target: '_blank' .form-check = f.radio_button :build_allow_git_fetch, 'false', { class: 'form-check-input' } @@ -47,7 +47,7 @@ = f.label :ci_config_path, _('Custom CI config path'), class: 'label-bold' = f.text_field :ci_config_path, class: 'form-control', placeholder: '.gitlab-ci.yml' %p.form-text.text-muted - = _("The path to CI config file. Defaults to <code>.gitlab-ci.yml</code>") + = _("The path to CI config file. Defaults to <code>.gitlab-ci.yml</code>").html_safe = link_to icon('question-circle'), help_page_path('user/project/pipelines/settings', anchor: 'custom-ci-config-path'), target: '_blank' %hr diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index c79d3dcf18d..aba289c790f 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -25,8 +25,8 @@ .nav-links.scrolling-tabs = render 'stat_anchor_list', anchors: @project.statistics_anchors(show_auto_devops_callout: show_auto_devops_callout) = render 'stat_anchor_list', anchors: @project.statistics_buttons(show_auto_devops_callout: show_auto_devops_callout) - - if Feature.enabled?(:repository_languages, @project.namespace.becomes(Namespace)) - = repository_languages_bar(@project.repository_languages) + + = repository_languages_bar(@project.repository_languages) %div{ class: [container_class, ("limit-container-width" unless fluid_layout)] } - if @project.archived? diff --git a/app/views/search/results/_blob.html.haml b/app/views/search/results/_blob.html.haml index fdcd126e7a3..a8d4d4af93a 100644 --- a/app/views/search/results/_blob.html.haml +++ b/app/views/search/results/_blob.html.haml @@ -1,4 +1,6 @@ - project = find_project_for_result_blob(blob) +- return unless project + - file_name, blob = parse_search_result(blob) - blob_link = project_blob_path(project, tree_join(blob.ref, file_name)) diff --git a/app/views/shared/_remote_mirror_update_button.html.haml b/app/views/shared/_remote_mirror_update_button.html.haml index 34de1c0695f..f32cff18fa8 100644 --- a/app/views/shared/_remote_mirror_update_button.html.haml +++ b/app/views/shared/_remote_mirror_update_button.html.haml @@ -1,13 +1,6 @@ -- if @project.has_remote_mirror? - .append-bottom-default - - if remote_mirror.update_in_progress? - %span.btn.disabled - = icon("refresh spin") - Updating… - - else - = link_to update_now_project_mirror_path(@project, sync_remote: true), method: :post, class: "btn" do - = icon("refresh") - Update Now - - if @remote_mirror.last_successful_update_at - %p.inline.prepend-left-10 - Successfully updated #{time_ago_with_tooltip(@remote_mirror.last_successful_update_at)}. +- if remote_mirror.update_in_progress? + %button.btn.disabled{ type: 'button', data: { toggle: 'tooltip', container: 'body' }, title: _('Updating') } + = icon("refresh spin") +- else + = link_to update_now_project_mirror_path(@project, sync_remote: true), method: :post, class: "btn", data: { toggle: 'tooltip', container: 'body' }, title: _('Update now') do + = icon("refresh") diff --git a/app/views/shared/milestones/_issuables.html.haml b/app/views/shared/milestones/_issuables.html.haml index ee6354b1c28..ee97f0172da 100644 --- a/app/views/shared/milestones/_issuables.html.haml +++ b/app/views/shared/milestones/_issuables.html.haml @@ -2,8 +2,8 @@ - primary = local_assigns.fetch(:primary, false) - panel_class = primary ? 'bg-primary text-white' : '' -.card{ class: panel_class } - .card-header +.card + .card-header{ class: panel_class } .title = title - if show_counter diff --git a/app/views/shared/notifications/_custom_notifications.html.haml b/app/views/shared/notifications/_custom_notifications.html.haml index 1f6e8f98bbb..43a87fd8397 100644 --- a/app/views/shared/notifications/_custom_notifications.html.haml +++ b/app/views/shared/notifications/_custom_notifications.html.haml @@ -19,7 +19,7 @@ - paragraph = _('Custom notification levels are the same as participating levels. With custom notification levels you will also receive notifications for select events. To find out more, check out %{notification_link}.') % { notification_link: notification_link.html_safe } #{ paragraph.html_safe } .col-lg-8 - - NotificationSetting::EMAIL_EVENTS.each_with_index do |event, index| + - notification_setting.email_events.each_with_index do |event, index| - field_id = "#{notifications_menu_identifier("modal", notification_setting)}_notification_setting[#{event}]" .form-group .form-check{ class: ("prepend-top-0" if index == 0) } diff --git a/app/views/shared/plugins/_index.html.haml b/app/views/shared/plugins/_index.html.haml index 7bcc54e7459..9d230d12be2 100644 --- a/app/views/shared/plugins/_index.html.haml +++ b/app/views/shared/plugins/_index.html.haml @@ -19,5 +19,5 @@ .monospace = File.basename(file) - else - %p.card.bg-light.text-center - No plugins found. + .card.bg-light.text-center + .nothing-here-block No plugins found. diff --git a/app/views/shared/runners/_form.html.haml b/app/views/shared/runners/_form.html.haml index 0337680d79b..fa93307be31 100644 --- a/app/views/shared/runners/_form.html.haml +++ b/app/views/shared/runners/_form.html.haml @@ -1,56 +1,56 @@ = form_for runner, url: runner_form_url do |f| = form_errors(runner) .form-group.row - = label :active, "Active", class: 'col-form-label col-sm-2' + = label :active, _("Active"), class: 'col-form-label col-sm-2' .col-sm-10 .form-check = f.check_box :active, { class: 'form-check-input' } - %span.light Paused Runners don't accept new jobs + %label.light{ for: :runner_active }= _("Paused Runners don't accept new jobs") .form-group.row - = label :protected, "Protected", class: 'col-form-label col-sm-2' + = label :protected, _("Protected"), class: 'col-form-label col-sm-2' .col-sm-10 .form-check = f.check_box :access_level, { class: 'form-check-input' }, 'ref_protected', 'not_protected' - %span.light This runner will only run on pipelines triggered on protected branches + %label.light{ for: :runner_access_level }= _('This runner will only run on pipelines triggered on protected branches') .form-group.row - = label :run_untagged, 'Run untagged jobs', class: 'col-form-label col-sm-2' + = label :run_untagged, _('Run untagged jobs'), class: 'col-form-label col-sm-2' .col-sm-10 .form-check = f.check_box :run_untagged, { class: 'form-check-input' } - %span.light Indicates whether this runner can pick jobs without tags + %label.light{ for: :runner_run_untagged }= _('Indicates whether this runner can pick jobs without tags') - unless runner.group_type? .form-group.row = label :locked, _('Lock to current projects'), class: 'col-form-label col-sm-2' .col-sm-10 .form-check = f.check_box :locked, { class: 'form-check-input' } - %span.light= _('When a runner is locked, it cannot be assigned to other projects') + %label.light{ for: :runner_locked }= _('When a runner is locked, it cannot be assigned to other projects') .form-group.row = label_tag :token, class: 'col-form-label col-sm-2' do - Token + = _('Token') .col-sm-10 = f.text_field :token, class: 'form-control', readonly: true .form-group.row = label_tag :ip_address, class: 'col-form-label col-sm-2' do - IP Address + = _('IP Address') .col-sm-10 = f.text_field :ip_address, class: 'form-control', readonly: true .form-group.row = label_tag :description, class: 'col-form-label col-sm-2' do - Description + = _('Description') .col-sm-10 = f.text_field :description, class: 'form-control' .form-group.row = label_tag :maximum_timeout_human_readable, class: 'col-form-label col-sm-2' do - Maximum job timeout + = _('Maximum job timeout') .col-sm-10 = f.text_field :maximum_timeout_human_readable, class: 'form-control' - .form-text.text-muted This timeout will take precedence when lower than Project-defined timeout + .form-text.text-muted= _('This timeout will take precedence when lower than Project-defined timeout') .form-group.row = label_tag :tag_list, class: 'col-form-label col-sm-2' do - Tags + = _('Tags') .col-sm-10 = f.text_field :tag_list, value: runner.tag_list.sort.join(', '), class: 'form-control' - .form-text.text-muted You can setup jobs to only use Runners with specific tags. Separate tags with commas. + .form-text.text-muted= _('You can setup jobs to only use Runners with specific tags. Separate tags with commas.') .form-actions - = f.submit 'Save changes', class: 'btn btn-save' + = f.submit _('Save changes'), class: 'btn btn-success' diff --git a/app/workers/detect_repository_languages_worker.rb b/app/workers/detect_repository_languages_worker.rb index 537b8fd5963..854b74b884a 100644 --- a/app/workers/detect_repository_languages_worker.rb +++ b/app/workers/detect_repository_languages_worker.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class DetectRepositoryLanguagesWorker include ApplicationWorker include ExceptionBacktrace @@ -14,8 +16,6 @@ class DetectRepositoryLanguagesWorker user = User.find_by(id: user_id) return unless project && user - return if Feature.disabled?(:repository_languages, project.namespace) - try_obtain_lease do ::Projects::DetectRepositoryLanguagesService.new(project, user).execute end diff --git a/app/workers/remove_expired_group_links_worker.rb b/app/workers/remove_expired_group_links_worker.rb index 6b8b972a440..25128caf72f 100644 --- a/app/workers/remove_expired_group_links_worker.rb +++ b/app/workers/remove_expired_group_links_worker.rb @@ -5,6 +5,6 @@ class RemoveExpiredGroupLinksWorker include CronjobQueue def perform - ProjectGroupLink.expired.destroy_all + ProjectGroupLink.expired.destroy_all # rubocop: disable DestroyAll end end diff --git a/app/workers/remove_old_web_hook_logs_worker.rb b/app/workers/remove_old_web_hook_logs_worker.rb index 17140ac4450..0f486f8991d 100644 --- a/app/workers/remove_old_web_hook_logs_worker.rb +++ b/app/workers/remove_old_web_hook_logs_worker.rb @@ -6,7 +6,9 @@ class RemoveOldWebHookLogsWorker WEB_HOOK_LOG_LIFETIME = 2.days + # rubocop: disable DestroyAll def perform WebHookLog.destroy_all(['created_at < ?', Time.now - WEB_HOOK_LOG_LIFETIME]) end + # rubocop: enable DestroyAll end diff --git a/app/workers/todos_destroyer/confidential_issue_worker.rb b/app/workers/todos_destroyer/confidential_issue_worker.rb index 9d640c14963..481fde8c83d 100644 --- a/app/workers/todos_destroyer/confidential_issue_worker.rb +++ b/app/workers/todos_destroyer/confidential_issue_worker.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module TodosDestroyer class ConfidentialIssueWorker include ApplicationWorker diff --git a/app/workers/todos_destroyer/entity_leave_worker.rb b/app/workers/todos_destroyer/entity_leave_worker.rb index e62d9876f4a..7db3f6c84b4 100644 --- a/app/workers/todos_destroyer/entity_leave_worker.rb +++ b/app/workers/todos_destroyer/entity_leave_worker.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module TodosDestroyer class EntityLeaveWorker include ApplicationWorker diff --git a/app/workers/todos_destroyer/group_private_worker.rb b/app/workers/todos_destroyer/group_private_worker.rb index 3e47eec7461..21ec4abe478 100644 --- a/app/workers/todos_destroyer/group_private_worker.rb +++ b/app/workers/todos_destroyer/group_private_worker.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module TodosDestroyer class GroupPrivateWorker include ApplicationWorker diff --git a/app/workers/todos_destroyer/private_features_worker.rb b/app/workers/todos_destroyer/private_features_worker.rb index f457d5e0471..1e68f0fd5ae 100644 --- a/app/workers/todos_destroyer/private_features_worker.rb +++ b/app/workers/todos_destroyer/private_features_worker.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module TodosDestroyer class PrivateFeaturesWorker include ApplicationWorker diff --git a/app/workers/todos_destroyer/project_private_worker.rb b/app/workers/todos_destroyer/project_private_worker.rb index 7a853c36370..064e001530c 100644 --- a/app/workers/todos_destroyer/project_private_worker.rb +++ b/app/workers/todos_destroyer/project_private_worker.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module TodosDestroyer class ProjectPrivateWorker include ApplicationWorker diff --git a/bin/secpick b/bin/secpick index 5029fe57cfe..5e30c8e72c5 100755 --- a/bin/secpick +++ b/bin/secpick @@ -35,7 +35,9 @@ parser.parse! abort("Missing options. Use #{$0} --help to see the list of options available".red) if options.values.include?(nil) abort("Wrong version format #{options[:version].bold}".red) unless options[:version] =~ /\A\d*\-\d*\Z/ -branch = [BRANCH_PREFIX, options[:branch], options[:version]].join('-').freeze +branch = "#{options[:branch]}-#{options[:version]}" +branch.prepend("#{BRANCH_PREFIX}-") unless branch.start_with?("#{BRANCH_PREFIX}-") +branch = branch.freeze stable_branch = "#{BRANCH_PREFIX}-#{options[:version]}".freeze command = "git fetch #{REMOTE} #{stable_branch} && git checkout #{stable_branch} && git pull #{REMOTE} #{stable_branch} && git checkout -B #{branch} && git cherry-pick #{options[:sha]} && git push #{REMOTE} #{branch}" diff --git a/changelogs/unreleased/25990-web-terminal-improvements.yml b/changelogs/unreleased/25990-web-terminal-improvements.yml new file mode 100644 index 00000000000..99a4a82ea66 --- /dev/null +++ b/changelogs/unreleased/25990-web-terminal-improvements.yml @@ -0,0 +1,5 @@ +--- +title: Make terminal button more visible +merge_request: +author: +type: changed diff --git a/changelogs/unreleased/2747-protected-environments-backend-ce.yml b/changelogs/unreleased/2747-protected-environments-backend-ce.yml new file mode 100644 index 00000000000..dcec74a33a7 --- /dev/null +++ b/changelogs/unreleased/2747-protected-environments-backend-ce.yml @@ -0,0 +1,5 @@ +--- +title: CE Port of Protected Environments backend +merge_request: 20859 +author: +type: other diff --git a/changelogs/unreleased/28930-add-project-reference-filter.yml b/changelogs/unreleased/28930-add-project-reference-filter.yml new file mode 100644 index 00000000000..c7679c5fe76 --- /dev/null +++ b/changelogs/unreleased/28930-add-project-reference-filter.yml @@ -0,0 +1,5 @@ +--- +title: Add the ability to reference projects in comments and other markdown text. +merge_request: 20285 +author: Reuben Pereira +type: added diff --git a/changelogs/unreleased/41738-fix-sorting-issues-is-wrong-in-list-with-pagination.yml b/changelogs/unreleased/41738-fix-sorting-issues-is-wrong-in-list-with-pagination.yml new file mode 100644 index 00000000000..bc0150c6586 --- /dev/null +++ b/changelogs/unreleased/41738-fix-sorting-issues-is-wrong-in-list-with-pagination.yml @@ -0,0 +1,5 @@ +---
+title: "Fix If-Check the result that a function was executed several times"
+merge_request: 20640
+author: Max Dicker
+type: fixed
diff --git a/changelogs/unreleased/41996-copy-to-clipboard-tooltip-appears-under-modal.yml b/changelogs/unreleased/41996-copy-to-clipboard-tooltip-appears-under-modal.yml new file mode 100644 index 00000000000..e452a91d8b7 --- /dev/null +++ b/changelogs/unreleased/41996-copy-to-clipboard-tooltip-appears-under-modal.yml @@ -0,0 +1,5 @@ +--- +title: Solve tooltip appears under modal +merge_request: 21017 +author: +type: fixed diff --git a/changelogs/unreleased/45663-tag-quick-action-on-commit-comments.yml b/changelogs/unreleased/45663-tag-quick-action-on-commit-comments.yml new file mode 100644 index 00000000000..6d664511e6e --- /dev/null +++ b/changelogs/unreleased/45663-tag-quick-action-on-commit-comments.yml @@ -0,0 +1,5 @@ +--- +title: "`/tag` quick action on Commit comments" +merge_request: 20694 +author: Peter Leitzen +type: added diff --git a/changelogs/unreleased/47752-buttons-on-new-file-page-wrap-outside-of-container-for-long-branch-names.yml b/changelogs/unreleased/47752-buttons-on-new-file-page-wrap-outside-of-container-for-long-branch-names.yml new file mode 100644 index 00000000000..81ca632947d --- /dev/null +++ b/changelogs/unreleased/47752-buttons-on-new-file-page-wrap-outside-of-container-for-long-branch-names.yml @@ -0,0 +1,5 @@ +--- +title: Fix buttons on the new file page wrapping outside of the container +merge_request: 21015 +author: +type: fixed diff --git a/changelogs/unreleased/47845-propagate_failure_reason-to-job-webhook.yml b/changelogs/unreleased/47845-propagate_failure_reason-to-job-webhook.yml new file mode 100644 index 00000000000..3f85f75bef4 --- /dev/null +++ b/changelogs/unreleased/47845-propagate_failure_reason-to-job-webhook.yml @@ -0,0 +1,5 @@ +--- +title: "#47845 Add failure_reason to job webhook" +merge_request: 21143 +author: matemaciek +type: added diff --git a/changelogs/unreleased/48320-cancel-a-created-job.yml b/changelogs/unreleased/48320-cancel-a-created-job.yml new file mode 100644 index 00000000000..3e7a9e9ae52 --- /dev/null +++ b/changelogs/unreleased/48320-cancel-a-created-job.yml @@ -0,0 +1,5 @@ +--- +title: Allows to cancel a Created job +merge_request: 20635 +author: Jacopo Beschi @jacopo-beschi +type: added diff --git a/changelogs/unreleased/48942-rename-backlog-list-to-open-issue-boards.yml b/changelogs/unreleased/48942-rename-backlog-list-to-open-issue-boards.yml new file mode 100644 index 00000000000..26851fc2dec --- /dev/null +++ b/changelogs/unreleased/48942-rename-backlog-list-to-open-issue-boards.yml @@ -0,0 +1,5 @@ +--- +title: Change 'Backlog' list title to 'Open' in Issue Boards +merge_request: 21131 +author: +type: changed diff --git a/changelogs/unreleased/48967-disable-statement-timeout.yml b/changelogs/unreleased/48967-disable-statement-timeout.yml new file mode 100644 index 00000000000..2da800ed41e --- /dev/null +++ b/changelogs/unreleased/48967-disable-statement-timeout.yml @@ -0,0 +1,5 @@ +--- +title: disable_statement_timeout no longer leak to other migrations +merge_request: 20503 +author: +type: fixed diff --git a/changelogs/unreleased/49770-fixes-input-alignment-on-user-admin-form-with-errors.yml b/changelogs/unreleased/49770-fixes-input-alignment-on-user-admin-form-with-errors.yml new file mode 100644 index 00000000000..00e1f6e638a --- /dev/null +++ b/changelogs/unreleased/49770-fixes-input-alignment-on-user-admin-form-with-errors.yml @@ -0,0 +1,5 @@ +--- +title: Fixes input alignment in user admin form with errors +merge_request: 21108 +author: Jacopo Beschi @jacopo-beschi +type: fixed diff --git a/changelogs/unreleased/49796-project-deletion-may-not-log-audit-events-during-group-deletion.yml b/changelogs/unreleased/49796-project-deletion-may-not-log-audit-events-during-group-deletion.yml new file mode 100644 index 00000000000..bb7633abdb1 --- /dev/null +++ b/changelogs/unreleased/49796-project-deletion-may-not-log-audit-events-during-group-deletion.yml @@ -0,0 +1,5 @@ +--- +title: 'Fix: Project deletion may not log audit events during group deletion' +merge_request: 21162 +author: +type: fixed diff --git a/changelogs/unreleased/49796-project-deletion-may-not-log-audit-events-during-user-deletion.yml b/changelogs/unreleased/49796-project-deletion-may-not-log-audit-events-during-user-deletion.yml new file mode 100644 index 00000000000..a8e3d590a4a --- /dev/null +++ b/changelogs/unreleased/49796-project-deletion-may-not-log-audit-events-during-user-deletion.yml @@ -0,0 +1,5 @@ +--- +title: 'Fix: Project deletion may not log audit events during user deletion' +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/49905-fix-checkboxes-runners.yml b/changelogs/unreleased/49905-fix-checkboxes-runners.yml new file mode 100644 index 00000000000..af40e5348b8 --- /dev/null +++ b/changelogs/unreleased/49905-fix-checkboxes-runners.yml @@ -0,0 +1,5 @@ +--- +title: Fix checkboxes on runner admin settings - The labels are now clickable +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/49907-commits-and-merge-requests-does-not-list-all-files-when-one-file-exceeds-size-limits.yml b/changelogs/unreleased/49907-commits-and-merge-requests-does-not-list-all-files-when-one-file-exceeds-size-limits.yml new file mode 100644 index 00000000000..2fce00a662f --- /dev/null +++ b/changelogs/unreleased/49907-commits-and-merge-requests-does-not-list-all-files-when-one-file-exceeds-size-limits.yml @@ -0,0 +1,5 @@ +--- +title: Fix merge requests not showing any diff files for big patches +merge_request: 21125 +author: +type: fixed diff --git a/changelogs/unreleased/49953-add-user_show_add_ssh_key_message-setting.yml b/changelogs/unreleased/49953-add-user_show_add_ssh_key_message-setting.yml new file mode 100644 index 00000000000..82423092792 --- /dev/null +++ b/changelogs/unreleased/49953-add-user_show_add_ssh_key_message-setting.yml @@ -0,0 +1,5 @@ +--- +title: Add ability to suppress the global "You won't be able to use SSH" message +merge_request: 21027 +author: Ævar Arnfjörð Bjarmason +type: added diff --git a/changelogs/unreleased/50019-remove-redundant-header-from-metrics-page.yml b/changelogs/unreleased/50019-remove-redundant-header-from-metrics-page.yml new file mode 100644 index 00000000000..8057819b223 --- /dev/null +++ b/changelogs/unreleased/50019-remove-redundant-header-from-metrics-page.yml @@ -0,0 +1,5 @@ +--- +title: Remove redundant header from metrics page +merge_request: 21282 +author: +type: changed diff --git a/changelogs/unreleased/50047-spam-logs-pagination.yml b/changelogs/unreleased/50047-spam-logs-pagination.yml new file mode 100644 index 00000000000..ca5f432cd8c --- /dev/null +++ b/changelogs/unreleased/50047-spam-logs-pagination.yml @@ -0,0 +1,5 @@ +--- +title: Add gitlab theme to spam logs pagination +merge_request: 21145 +author: +type: fixed diff --git a/changelogs/unreleased/50063-add-missing-i18n-strings-to-issue-boards.yml b/changelogs/unreleased/50063-add-missing-i18n-strings-to-issue-boards.yml new file mode 100644 index 00000000000..ca17a41d611 --- /dev/null +++ b/changelogs/unreleased/50063-add-missing-i18n-strings-to-issue-boards.yml @@ -0,0 +1,5 @@ +--- +title: Added missing i18n strings to issue boards lables dropdown +merge_request: 21081 +author: +type: other diff --git a/changelogs/unreleased/50101-aritfacts-block.yml b/changelogs/unreleased/50101-aritfacts-block.yml new file mode 100644 index 00000000000..435e9d9d486 --- /dev/null +++ b/changelogs/unreleased/50101-aritfacts-block.yml @@ -0,0 +1,5 @@ +--- +title: Creates Vue component for artifacts block on job page +merge_request: +author: +type: other diff --git a/changelogs/unreleased/50101-builds-dropdown.yml b/changelogs/unreleased/50101-builds-dropdown.yml new file mode 100644 index 00000000000..9194b0e0d31 --- /dev/null +++ b/changelogs/unreleased/50101-builds-dropdown.yml @@ -0,0 +1,6 @@ +--- +title: Creates vue components for stage dropdowns and job list container for job log + view +merge_request: +author: +type: other diff --git a/changelogs/unreleased/50101-commit-block.yml b/changelogs/unreleased/50101-commit-block.yml new file mode 100644 index 00000000000..f6bad4c8154 --- /dev/null +++ b/changelogs/unreleased/50101-commit-block.yml @@ -0,0 +1,5 @@ +--- +title: Creates vue component for commit block in job log page +merge_request: +author: +type: other diff --git a/changelogs/unreleased/50101-empty-state-component.yml b/changelogs/unreleased/50101-empty-state-component.yml new file mode 100644 index 00000000000..ee99b65d964 --- /dev/null +++ b/changelogs/unreleased/50101-empty-state-component.yml @@ -0,0 +1,5 @@ +--- +title: Creates empty state vue component for job view +merge_request: +author: +type: other diff --git a/changelogs/unreleased/50101-erased-block.yml b/changelogs/unreleased/50101-erased-block.yml new file mode 100644 index 00000000000..5a5c9bc0fc4 --- /dev/null +++ b/changelogs/unreleased/50101-erased-block.yml @@ -0,0 +1,5 @@ +--- +title: Creates vue component for erased block on job view +merge_request: +author: +type: other diff --git a/changelogs/unreleased/50101-job-log-component.yml b/changelogs/unreleased/50101-job-log-component.yml new file mode 100644 index 00000000000..0759e7cfbd9 --- /dev/null +++ b/changelogs/unreleased/50101-job-log-component.yml @@ -0,0 +1,5 @@ +--- +title: Creates vue component for job log trace +merge_request: +author: +type: other diff --git a/changelogs/unreleased/50101-stuck-component.yml b/changelogs/unreleased/50101-stuck-component.yml new file mode 100644 index 00000000000..bfe4009a2b3 --- /dev/null +++ b/changelogs/unreleased/50101-stuck-component.yml @@ -0,0 +1,5 @@ +--- +title: Creates Vvue component for warning block about stuck runners +merge_request: +author: +type: other diff --git a/changelogs/unreleased/50101-trigger.yml b/changelogs/unreleased/50101-trigger.yml new file mode 100644 index 00000000000..df4243afa63 --- /dev/null +++ b/changelogs/unreleased/50101-trigger.yml @@ -0,0 +1,5 @@ +--- +title: Creates Vue component for trigger variables block in job log page +merge_request: +author: +type: other diff --git a/changelogs/unreleased/50101-truncated-job-information.yml b/changelogs/unreleased/50101-truncated-job-information.yml new file mode 100644 index 00000000000..b873b8b7bf6 --- /dev/null +++ b/changelogs/unreleased/50101-truncated-job-information.yml @@ -0,0 +1,5 @@ +--- +title: Creates vue component for job log top bar with controllers +merge_request: +author: +type: other diff --git a/changelogs/unreleased/50126-blocked-user-card.yml b/changelogs/unreleased/50126-blocked-user-card.yml new file mode 100644 index 00000000000..a42d62e5530 --- /dev/null +++ b/changelogs/unreleased/50126-blocked-user-card.yml @@ -0,0 +1,5 @@ +--- +title: Fix blocked user card style +merge_request: 21095 +author: +type: fixed diff --git a/changelogs/unreleased/50180-fa-icon-google-audit.yml b/changelogs/unreleased/50180-fa-icon-google-audit.yml new file mode 100644 index 00000000000..fb1771a7570 --- /dev/null +++ b/changelogs/unreleased/50180-fa-icon-google-audit.yml @@ -0,0 +1,5 @@ +--- +title: Show google icon in audit log +merge_request: 21207 +author: Jan Beckmann +type: fixed diff --git a/changelogs/unreleased/50243-auto-devops-behind-a-proxy.yml b/changelogs/unreleased/50243-auto-devops-behind-a-proxy.yml new file mode 100644 index 00000000000..0f6208d0c7a --- /dev/null +++ b/changelogs/unreleased/50243-auto-devops-behind-a-proxy.yml @@ -0,0 +1,6 @@ +--- +title: Vendor Auto-DevOps.gitlab-ci.yml with new proxy env vars passed through to + docker +merge_request: 21159 +author: kinolaev +type: added diff --git a/changelogs/unreleased/50257-fix-auto-devops-glibc-pubkey-url.yml b/changelogs/unreleased/50257-fix-auto-devops-glibc-pubkey-url.yml new file mode 100644 index 00000000000..e081dfe6093 --- /dev/null +++ b/changelogs/unreleased/50257-fix-auto-devops-glibc-pubkey-url.yml @@ -0,0 +1,5 @@ +--- +title: 'Auto-DevOps.gitlab-ci.yml: Update glibc package signing key URL' +merge_request: 21182 +author: sgerrand +type: fixed diff --git a/changelogs/unreleased/50281-js-pages-do-not-load-on-windows-8-ie-11.yml b/changelogs/unreleased/50281-js-pages-do-not-load-on-windows-8-ie-11.yml new file mode 100644 index 00000000000..eb20e34c466 --- /dev/null +++ b/changelogs/unreleased/50281-js-pages-do-not-load-on-windows-8-ie-11.yml @@ -0,0 +1,5 @@ +--- +title: Fix broken JavaScript in IE11 +merge_request: 21214 +author: +type: fixed diff --git a/changelogs/unreleased/50312-instance-statistics-convdev-index-intro-banner-is-not-dismissable.yml b/changelogs/unreleased/50312-instance-statistics-convdev-index-intro-banner-is-not-dismissable.yml new file mode 100644 index 00000000000..50a3b9c9aff --- /dev/null +++ b/changelogs/unreleased/50312-instance-statistics-convdev-index-intro-banner-is-not-dismissable.yml @@ -0,0 +1,5 @@ +--- +title: Fix issue stopping Instance Statistics javascript to be executed +merge_request: 21211 +author: +type: fixed diff --git a/changelogs/unreleased/6010_remove_gemnasium_service.yml b/changelogs/unreleased/6010_remove_gemnasium_service.yml new file mode 100644 index 00000000000..900e84c9eed --- /dev/null +++ b/changelogs/unreleased/6010_remove_gemnasium_service.yml @@ -0,0 +1,5 @@ +--- +title: Remove Gemnasium service +merge_request: 21185 +author: +type: removed diff --git a/changelogs/unreleased/ab-49446-internal-ids-inconsistency.yml b/changelogs/unreleased/ab-49446-internal-ids-inconsistency.yml new file mode 100644 index 00000000000..bfea57d79e0 --- /dev/null +++ b/changelogs/unreleased/ab-49446-internal-ids-inconsistency.yml @@ -0,0 +1,5 @@ +--- +title: Add migration to cleanup internal_ids inconsistency. +merge_request: 20926 +author: +type: fixed diff --git a/changelogs/unreleased/add-ci_archive_traces_cron_worker-to-gitlab-yml.yml b/changelogs/unreleased/add-ci_archive_traces_cron_worker-to-gitlab-yml.yml new file mode 100644 index 00000000000..d963dc5bac3 --- /dev/null +++ b/changelogs/unreleased/add-ci_archive_traces_cron_worker-to-gitlab-yml.yml @@ -0,0 +1,5 @@ +--- +title: Add an example of the configuration of archive trace cron worker in gitlab.yml.example +merge_request: 20583 +author: +type: other diff --git a/changelogs/unreleased/add-rake-command-to-migrate-locally-persisted-archived-traces.yml b/changelogs/unreleased/add-rake-command-to-migrate-locally-persisted-archived-traces.yml new file mode 100644 index 00000000000..b82344e3c9c --- /dev/null +++ b/changelogs/unreleased/add-rake-command-to-migrate-locally-persisted-archived-traces.yml @@ -0,0 +1,5 @@ +--- +title: Add rake command to migrate archived traces from local storage to object storage +merge_request: 21193 +author: +type: added diff --git a/changelogs/unreleased/add_google_noto_color_emoji_font.yml b/changelogs/unreleased/add_google_noto_color_emoji_font.yml new file mode 100644 index 00000000000..9ba46262767 --- /dev/null +++ b/changelogs/unreleased/add_google_noto_color_emoji_font.yml @@ -0,0 +1,5 @@ +--- +title: Add Noto Color Emoji font support +merge_request: 19036 +author: Alexander Popov +type: changed diff --git a/changelogs/unreleased/auto-devops-gitlab-ci-glic-228.yml b/changelogs/unreleased/auto-devops-gitlab-ci-glic-228.yml new file mode 100644 index 00000000000..a1625193189 --- /dev/null +++ b/changelogs/unreleased/auto-devops-gitlab-ci-glic-228.yml @@ -0,0 +1,5 @@ +--- +title: 'Auto-DevOps.gitlab-ci.yml: update glibc package to 2.28' +merge_request: 21191 +author: sgerrand +type: fixed diff --git a/changelogs/unreleased/bvl-add-czech.yml b/changelogs/unreleased/bvl-add-czech.yml new file mode 100644 index 00000000000..49e0e4a74b7 --- /dev/null +++ b/changelogs/unreleased/bvl-add-czech.yml @@ -0,0 +1,5 @@ +--- +title: Add Czech as an available language. +merge_request: 21201 +author: +type: added diff --git a/changelogs/unreleased/bvl-merge-base-api.yml b/changelogs/unreleased/bvl-merge-base-api.yml new file mode 100644 index 00000000000..78fb3ce0897 --- /dev/null +++ b/changelogs/unreleased/bvl-merge-base-api.yml @@ -0,0 +1,5 @@ +--- +title: Get the merge base of two refs through the API +merge_request: 20929 +author: +type: added diff --git a/changelogs/unreleased/ce-5666-optimize_querying_manageable_groups.yml b/changelogs/unreleased/ce-5666-optimize_querying_manageable_groups.yml new file mode 100644 index 00000000000..0c6a1071cdd --- /dev/null +++ b/changelogs/unreleased/ce-5666-optimize_querying_manageable_groups.yml @@ -0,0 +1,5 @@ +--- +title: Optimize querying User#manageable_groups +merge_request: 21050 +author: +type: performance diff --git a/changelogs/unreleased/emoji-cutoff-1px.yml b/changelogs/unreleased/emoji-cutoff-1px.yml new file mode 100644 index 00000000000..815d9c177e8 --- /dev/null +++ b/changelogs/unreleased/emoji-cutoff-1px.yml @@ -0,0 +1,5 @@ +--- +title: Fix 1px cutoff of emojis +merge_request: 21180 +author: gfyoung +type: fixed diff --git a/changelogs/unreleased/expose-all-artifacts-sizes-in-jobs-api.yml b/changelogs/unreleased/expose-all-artifacts-sizes-in-jobs-api.yml new file mode 100644 index 00000000000..1453d39934b --- /dev/null +++ b/changelogs/unreleased/expose-all-artifacts-sizes-in-jobs-api.yml @@ -0,0 +1,5 @@ +--- +title: Expose all artifacts sizes in jobs api +merge_request: 20821 +author: Peter Marko +type: added diff --git a/changelogs/unreleased/feat-add-default-avatar-to-group.yml b/changelogs/unreleased/feat-add-default-avatar-to-group.yml new file mode 100644 index 00000000000..56d8f2ccd6d --- /dev/null +++ b/changelogs/unreleased/feat-add-default-avatar-to-group.yml @@ -0,0 +1,5 @@ +--- +title: Add default avatar to group +merge_request: 17271 +author: George Tsiolis +type: changed diff --git a/changelogs/unreleased/fix-labels-list-item-height-with-no-description.yml b/changelogs/unreleased/fix-labels-list-item-height-with-no-description.yml new file mode 100644 index 00000000000..d215d034917 --- /dev/null +++ b/changelogs/unreleased/fix-labels-list-item-height-with-no-description.yml @@ -0,0 +1,5 @@ +--- +title: Fix label list item container height when there is no label description +merge_request: 21106 +author: +type: fixed diff --git a/changelogs/unreleased/fix-pipeline-fixture-seeder.yml b/changelogs/unreleased/fix-pipeline-fixture-seeder.yml new file mode 100644 index 00000000000..02b83062e07 --- /dev/null +++ b/changelogs/unreleased/fix-pipeline-fixture-seeder.yml @@ -0,0 +1,5 @@ +--- +title: Fix pipeline fixture seeder +merge_request: 21088 +author: +type: fixed diff --git a/changelogs/unreleased/fl-reduce-ee-conflicts-reports-code.yml b/changelogs/unreleased/fl-reduce-ee-conflicts-reports-code.yml new file mode 100644 index 00000000000..068681dfe19 --- /dev/null +++ b/changelogs/unreleased/fl-reduce-ee-conflicts-reports-code.yml @@ -0,0 +1,5 @@ +--- +title: Reduce differences between CE and EE code base in reports components +merge_request: +author: +type: other diff --git a/changelogs/unreleased/frozen-string-enable-app-mailers.yml b/changelogs/unreleased/frozen-string-enable-app-mailers.yml new file mode 100644 index 00000000000..2cd247ca76c --- /dev/null +++ b/changelogs/unreleased/frozen-string-enable-app-mailers.yml @@ -0,0 +1,5 @@ +--- +title: Enable frozen in app/mailers/**/*.rb +merge_request: 21147 +author: gfyoung +type: performance diff --git a/changelogs/unreleased/frozen-string-enable-app-models-even-more-still.yml b/changelogs/unreleased/frozen-string-enable-app-models-even-more-still.yml new file mode 100644 index 00000000000..a77f3baeed3 --- /dev/null +++ b/changelogs/unreleased/frozen-string-enable-app-models-even-more-still.yml @@ -0,0 +1,5 @@ +--- +title: Enable frozen string in rest of app/models/**/*.rb +merge_request: gfyoung +author: +type: performance diff --git a/changelogs/unreleased/frozen-string-enable-app-vestigial.yml b/changelogs/unreleased/frozen-string-enable-app-vestigial.yml new file mode 100644 index 00000000000..8cb7bd43784 --- /dev/null +++ b/changelogs/unreleased/frozen-string-enable-app-vestigial.yml @@ -0,0 +1,5 @@ +--- +title: Enable frozen string in vestigial app files +merge_request: +author: gfyoung +type: performance diff --git a/changelogs/unreleased/gitaly-install-path.yml b/changelogs/unreleased/gitaly-install-path.yml new file mode 100644 index 00000000000..4b24cd81dc7 --- /dev/null +++ b/changelogs/unreleased/gitaly-install-path.yml @@ -0,0 +1,5 @@ +--- +title: Remove storage path dependency of gitaly install task +merge_request: 21101 +author: +type: changed diff --git a/changelogs/unreleased/ide-delete-new-files-state.yml b/changelogs/unreleased/ide-delete-new-files-state.yml new file mode 100644 index 00000000000..500115d19d0 --- /dev/null +++ b/changelogs/unreleased/ide-delete-new-files-state.yml @@ -0,0 +1,5 @@ +--- +title: Fixed IDE deleting new files creating wrong state +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/ide-header-buttons-tooltip.yml b/changelogs/unreleased/ide-header-buttons-tooltip.yml new file mode 100644 index 00000000000..4c8f6fd554f --- /dev/null +++ b/changelogs/unreleased/ide-header-buttons-tooltip.yml @@ -0,0 +1,5 @@ +--- +title: Added tooltips to tree list header +merge_request: 21138 +author: +type: added diff --git a/changelogs/unreleased/ide-job-top-bar-ui-polish.yml b/changelogs/unreleased/ide-job-top-bar-ui-polish.yml new file mode 100644 index 00000000000..d917c14e5f8 --- /dev/null +++ b/changelogs/unreleased/ide-job-top-bar-ui-polish.yml @@ -0,0 +1,5 @@ +--- +title: Improved styling of top bar in IDE job trace pane +merge_request: +author: +type: changed diff --git a/changelogs/unreleased/ide-open-empty-merge-request.yml b/changelogs/unreleased/ide-open-empty-merge-request.yml new file mode 100644 index 00000000000..05f2de5d31c --- /dev/null +++ b/changelogs/unreleased/ide-open-empty-merge-request.yml @@ -0,0 +1,5 @@ +--- +title: Fix empty merge requests not opening in the Web IDE +merge_request: 21102 +author: +type: fixed diff --git a/changelogs/unreleased/mk-bump-rainbow-gem.yml b/changelogs/unreleased/mk-bump-rainbow-gem.yml new file mode 100644 index 00000000000..31c003fb4d9 --- /dev/null +++ b/changelogs/unreleased/mk-bump-rainbow-gem.yml @@ -0,0 +1,5 @@ +--- +title: Fix bin/secpick error and security branch prefixing +merge_request: 21210 +author: +type: fixed diff --git a/changelogs/unreleased/n8rzz-consolidate-specs-testing-emoji-awards.yml b/changelogs/unreleased/n8rzz-consolidate-specs-testing-emoji-awards.yml new file mode 100644 index 00000000000..bcf3d2c8e16 --- /dev/null +++ b/changelogs/unreleased/n8rzz-consolidate-specs-testing-emoji-awards.yml @@ -0,0 +1,6 @@ +--- +title: Combines emoji award spec files into single user_interacts_with_awards_in_issue_spec.rb + file +merge_request: 21126 +author: Nate Geslin +type: other diff --git a/changelogs/unreleased/rails5-fix-duplicate-gpg-signature.yml b/changelogs/unreleased/rails5-fix-duplicate-gpg-signature.yml new file mode 100644 index 00000000000..e31768773b1 --- /dev/null +++ b/changelogs/unreleased/rails5-fix-duplicate-gpg-signature.yml @@ -0,0 +1,5 @@ +--- +title: Rails5 fix specs duplicate key value violates unique constraint 'index_gpg_signatures_on_commit_sha' +merge_request: 21119 +author: Jasper Maes +type: fixed diff --git a/changelogs/unreleased/rails5-verbose-query-logs.yml b/changelogs/unreleased/rails5-verbose-query-logs.yml new file mode 100644 index 00000000000..7585e75d30b --- /dev/null +++ b/changelogs/unreleased/rails5-verbose-query-logs.yml @@ -0,0 +1,5 @@ +--- +title: 'Rails5: Enable verbose query logs' +merge_request: 21231 +author: Jasper Maes +type: other diff --git a/changelogs/unreleased/repopulate_site_statistics.yml b/changelogs/unreleased/repopulate_site_statistics.yml new file mode 100644 index 00000000000..1961088061d --- /dev/null +++ b/changelogs/unreleased/repopulate_site_statistics.yml @@ -0,0 +1,5 @@ +--- +title: Migrate NULL wiki_access_level to correct number so we count active wikis correctly +merge_request: 21030 +author: +type: changed diff --git a/changelogs/unreleased/rouge_3-2-1.yml b/changelogs/unreleased/rouge_3-2-1.yml new file mode 100644 index 00000000000..b281a4f0e95 --- /dev/null +++ b/changelogs/unreleased/rouge_3-2-1.yml @@ -0,0 +1,5 @@ +--- +title: Update to Rouge 3.2.1, which includes a critical fix to the Perl Lexer +merge_request: 21263 +author: +type: changed diff --git a/changelogs/unreleased/runners-online.yml b/changelogs/unreleased/runners-online.yml new file mode 100644 index 00000000000..a732d9cb723 --- /dev/null +++ b/changelogs/unreleased/runners-online.yml @@ -0,0 +1,5 @@ +--- +title: Clarify current runners online text +merge_request: 21151 +author: Ben Bodenmiller +type: other diff --git a/changelogs/unreleased/sh-bump-gitaly-for-11-2.yml b/changelogs/unreleased/sh-bump-gitaly-for-11-2.yml new file mode 100644 index 00000000000..0e748c3a346 --- /dev/null +++ b/changelogs/unreleased/sh-bump-gitaly-for-11-2.yml @@ -0,0 +1,5 @@ +--- +title: Bump Gitaly to 0.117.1 for Rouge update +merge_request: 21277 +author: +type: security diff --git a/changelogs/unreleased/sh-bump-rugged-0-27-4.yml b/changelogs/unreleased/sh-bump-rugged-0-27-4.yml new file mode 100644 index 00000000000..50373cb81ad --- /dev/null +++ b/changelogs/unreleased/sh-bump-rugged-0-27-4.yml @@ -0,0 +1,5 @@ +--- +title: Bump rugged to 0.27.4 for security fixes +merge_request: +author: +type: security diff --git a/changelogs/unreleased/tz-mr-incremental-rendering.yml b/changelogs/unreleased/tz-mr-incremental-rendering.yml new file mode 100644 index 00000000000..a35fa200363 --- /dev/null +++ b/changelogs/unreleased/tz-mr-incremental-rendering.yml @@ -0,0 +1,4 @@ +title: Incremental rendering with Vue on merge request page +merge_request: 21063 +author: +type: performance diff --git a/changelogs/unreleased/visual-improvements-language-bar.yml b/changelogs/unreleased/visual-improvements-language-bar.yml new file mode 100644 index 00000000000..23cae22b962 --- /dev/null +++ b/changelogs/unreleased/visual-improvements-language-bar.yml @@ -0,0 +1,5 @@ +--- +title: Improve visuals of language bar on projects +merge_request: 21006 +author: +type: changed diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index 561ff57c9fb..4847a82236b 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -261,6 +261,9 @@ production: &base # once per hour you will have concurrent 'git fsck' jobs. repository_check_worker: cron: "20 * * * *" + # Archive live traces which have not been archived yet + ci_archive_traces_cron_worker: + cron: "17 * * * *" # Send admin emails once a week admin_email_worker: cron: "0 0 * * 0" diff --git a/config/initializers/active_record_verbose_query_logs.rb b/config/initializers/active_record_verbose_query_logs.rb index 44f86fec7e0..1c5fbc8e830 100644 --- a/config/initializers/active_record_verbose_query_logs.rb +++ b/config/initializers/active_record_verbose_query_logs.rb @@ -47,7 +47,9 @@ module ActiveRecord end end - unless Gitlab.rails5? + if Rails.version.start_with?("5.2") + raise "Remove this monkey patch: #{__FILE__}" + else prepend(VerboseQueryLogs) unless Rails.env.production? end end diff --git a/config/initializers/gettext_rails_i18n_patch.rb b/config/initializers/gettext_rails_i18n_patch.rb index 49551319435..c1342f48ebd 100644 --- a/config/initializers/gettext_rails_i18n_patch.rb +++ b/config/initializers/gettext_rails_i18n_patch.rb @@ -1,5 +1,6 @@ require 'gettext_i18n_rails/haml_parser' require 'gettext_i18n_rails_js/parser/javascript' +require 'json' VUE_TRANSLATE_REGEX = /((%[\w.-]+)(?:\s))?{{ (N|n|s)?__\((.*)\) }}/ @@ -36,6 +37,20 @@ module GettextI18nRailsJs ".vue" ].include? ::File.extname(file) end + + def collect_for(file) + gettext_messages_by_file[file] || [] + end + + private + + def gettext_messages_by_file + @gettext_messages_by_file ||= JSON.parse(load_messages) + end + + def load_messages + `node scripts/frontend/extract_gettext_all.js --all` + end end end end diff --git a/db/fixtures/development/14_pipelines.rb b/db/fixtures/development/14_pipelines.rb index d3a63aa2a78..5535c4a14e5 100644 --- a/db/fixtures/development/14_pipelines.rb +++ b/db/fixtures/development/14_pipelines.rb @@ -41,7 +41,7 @@ class Gitlab::Seeder::Pipelines when: 'manual', status: :skipped }, # notify stage - { name: 'slack', stage: 'notify', when: 'manual', status: :created }, + { name: 'slack', stage: 'notify', when: 'manual', status: :success }, ] EXTERNAL_JOBS = [ { name: 'jenkins', stage: 'test', status: :success, @@ -54,16 +54,10 @@ class Gitlab::Seeder::Pipelines def seed! pipelines.each do |pipeline| - begin - BUILDS.each { |opts| build_create!(pipeline, opts) } - EXTERNAL_JOBS.each { |opts| commit_status_create!(pipeline, opts) } - print '.' - rescue ActiveRecord::RecordInvalid - print 'F' - ensure - pipeline.update_duration - pipeline.update_status - end + BUILDS.each { |opts| build_create!(pipeline, opts) } + EXTERNAL_JOBS.each { |opts| commit_status_create!(pipeline, opts) } + pipeline.update_duration + pipeline.update_status end end @@ -87,7 +81,9 @@ class Gitlab::Seeder::Pipelines branch = merge_request.source_branch merge_request.commits.last(4).map do |commit| - create_pipeline!(project, branch, commit) + create_pipeline!(project, branch, commit).tap do |pipeline| + merge_request.update!(head_pipeline_id: pipeline.id) + end end end @@ -98,7 +94,7 @@ class Gitlab::Seeder::Pipelines def create_pipeline!(project, ref, commit) - project.pipelines.create(sha: commit.id, ref: ref, source: :push) + project.pipelines.create!(sha: commit.id, ref: ref, source: :push) end def build_create!(pipeline, opts = {}) @@ -111,24 +107,39 @@ class Gitlab::Seeder::Pipelines # block directly to `Ci::Build#create!`. setup_artifacts(build) + setup_test_reports(build) setup_build_log(build) build.project.environments. find_or_create_by(name: build.expanded_environment_name) - build.save + build.save! end end def setup_artifacts(build) - return unless %w[build test].include?(build.stage) + return unless build.stage == "build" artifacts_cache_file(artifacts_archive_path) do |file| - build.job_artifacts.build(project: build.project, file_type: :archive, file: file) + build.job_artifacts.build(project: build.project, file_type: :archive, file_format: :zip, file: file) end artifacts_cache_file(artifacts_metadata_path) do |file| - build.job_artifacts.build(project: build.project, file_type: :metadata, file: file) + build.job_artifacts.build(project: build.project, file_type: :metadata, file_format: :gzip, file: file) + end + end + + def setup_test_reports(build) + return unless build.stage == "test" && build.name == "rspec:osx" + + if build.ref == build.project.default_branch + artifacts_cache_file(test_reports_pass_path) do |file| + build.job_artifacts.build(project: build.project, file_type: :junit, file_format: :gzip, file: file) + end + else + artifacts_cache_file(test_reports_failed_path) do |file| + build.job_artifacts.build(project: build.project, file_type: :junit, file_format: :gzip, file: file) + end end end @@ -171,13 +182,21 @@ class Gitlab::Seeder::Pipelines Rails.root + 'spec/fixtures/ci_build_artifacts_metadata.gz' end + def test_reports_pass_path + Rails.root + 'spec/fixtures/junit/junit_ant.xml.gz' + end + + def test_reports_failed_path + Rails.root + 'spec/fixtures/junit/junit.xml.gz' + end + def artifacts_cache_file(file_path) - cache_path = file_path.to_s.gsub('ci_', "p#{@project.id}_") + file = Tempfile.new("artifacts") + file.close - FileUtils.copy(file_path, cache_path) - File.open(cache_path) do |file| - yield file - end + FileUtils.copy(file_path, file.path) + + yield(UploadedFile.new(file.path, filename: File.basename(file_path))) end end diff --git a/db/fixtures/development/23_spam_logs.rb b/db/fixtures/development/23_spam_logs.rb new file mode 100644 index 00000000000..81cc13e6b2d --- /dev/null +++ b/db/fixtures/development/23_spam_logs.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +module Db + module Fixtures + module Development + class SpamLog + def self.seed + Gitlab::Seeder.quiet do + (::SpamLog.default_per_page + 3).times do |i| + ::SpamLog.create( + user: self.random_user, + user_agent: FFaker::Lorem.sentence, + source_ip: FFaker::Internet.ip_v4_address, + title: FFaker::Lorem.sentence, + description: FFaker::Lorem.paragraph, + via_api: FFaker::Boolean.random, + submitted_as_ham: FFaker::Boolean.random, + recaptcha_verified: FFaker::Boolean.random) + print '.' + end + end + end + + def self.random_user + User.find(User.pluck(:id).sample) + end + end + end + end +end + +Db::Fixtures::Development::SpamLog.seed diff --git a/db/migrate/20160317092222_add_moved_to_to_issue.rb b/db/migrate/20160317092222_add_moved_to_to_issue.rb index 461e7fb3a9b..2bf549d7ecd 100644 --- a/db/migrate/20160317092222_add_moved_to_to_issue.rb +++ b/db/migrate/20160317092222_add_moved_to_to_issue.rb @@ -1,5 +1,5 @@ class AddMovedToToIssue < ActiveRecord::Migration def change - add_reference :issues, :moved_to, references: :issues + add_reference :issues, :moved_to, references: :issues # rubocop:disable Migration/AddReference end end diff --git a/db/migrate/20160712171823_remove_award_emojis_with_no_user.rb b/db/migrate/20160712171823_remove_award_emojis_with_no_user.rb index 668c22bb51c..8ebf1a5234d 100644 --- a/db/migrate/20160712171823_remove_award_emojis_with_no_user.rb +++ b/db/migrate/20160712171823_remove_award_emojis_with_no_user.rb @@ -16,6 +16,6 @@ class RemoveAwardEmojisWithNoUser < ActiveRecord::Migration # disable_ddl_transaction! def up - AwardEmoji.joins('LEFT JOIN users ON users.id = user_id').where('users.id IS NULL').destroy_all + AwardEmoji.joins('LEFT JOIN users ON users.id = user_id').where('users.id IS NULL').destroy_all # rubocop: disable DestroyAll end end diff --git a/db/migrate/20170530130129_project_foreign_keys_with_cascading_deletes.rb b/db/migrate/20170530130129_project_foreign_keys_with_cascading_deletes.rb index 1199073ed3a..12352d98a62 100644 --- a/db/migrate/20170530130129_project_foreign_keys_with_cascading_deletes.rb +++ b/db/migrate/20170530130129_project_foreign_keys_with_cascading_deletes.rb @@ -106,11 +106,11 @@ class ProjectForeignKeysWithCascadingDeletes < ActiveRecord::Migration # Disables statement timeouts for the current connection. This is # necessary as removing of orphaned data might otherwise exceed the # statement timeout. - disable_statement_timeout + disable_statement_timeout do + remove_orphans(*queue.pop) until queue.empty? - remove_orphans(*queue.pop) until queue.empty? - - steal_from_queues(queues - [queue]) + steal_from_queues(queues - [queue]) + end end end end diff --git a/db/migrate/20170724214302_add_lower_path_index_to_redirect_routes.rb b/db/migrate/20170724214302_add_lower_path_index_to_redirect_routes.rb index db60c2087b9..a770ff63b4e 100644 --- a/db/migrate/20170724214302_add_lower_path_index_to_redirect_routes.rb +++ b/db/migrate/20170724214302_add_lower_path_index_to_redirect_routes.rb @@ -25,8 +25,9 @@ class AddLowerPathIndexToRedirectRoutes < ActiveRecord::Migration # trivial to write a query that checks for an index. BUT there is a # convenient `IF EXISTS` parameter for `DROP INDEX`. if supports_drop_index_concurrently? - disable_statement_timeout - execute "DROP INDEX CONCURRENTLY IF EXISTS #{INDEX_NAME};" + disable_statement_timeout do + execute "DROP INDEX CONCURRENTLY IF EXISTS #{INDEX_NAME};" + end else execute "DROP INDEX IF EXISTS #{INDEX_NAME};" end diff --git a/db/migrate/20171220191323_add_index_on_namespaces_lower_name.rb b/db/migrate/20171220191323_add_index_on_namespaces_lower_name.rb index d1a039ed551..130b24fe6f0 100644 --- a/db/migrate/20171220191323_add_index_on_namespaces_lower_name.rb +++ b/db/migrate/20171220191323_add_index_on_namespaces_lower_name.rb @@ -8,25 +8,25 @@ class AddIndexOnNamespacesLowerName < ActiveRecord::Migration def up return unless Gitlab::Database.postgresql? - disable_statement_timeout - - if Gitlab::Database.version.to_f >= 9.5 - # Allow us to hot-patch the index manually ahead of the migration - execute "CREATE INDEX CONCURRENTLY IF NOT EXISTS #{INDEX_NAME} ON namespaces (lower(name));" - else - execute "CREATE INDEX CONCURRENTLY #{INDEX_NAME} ON namespaces (lower(name));" + disable_statement_timeout do + if Gitlab::Database.version.to_f >= 9.5 + # Allow us to hot-patch the index manually ahead of the migration + execute "CREATE INDEX CONCURRENTLY IF NOT EXISTS #{INDEX_NAME} ON namespaces (lower(name));" + else + execute "CREATE INDEX CONCURRENTLY #{INDEX_NAME} ON namespaces (lower(name));" + end end end def down return unless Gitlab::Database.postgresql? - disable_statement_timeout - - if Gitlab::Database.version.to_f >= 9.2 - execute "DROP INDEX CONCURRENTLY IF EXISTS #{INDEX_NAME};" - else - execute "DROP INDEX IF EXISTS #{INDEX_NAME};" + disable_statement_timeout do + if Gitlab::Database.version.to_f >= 9.2 + execute "DROP INDEX CONCURRENTLY IF EXISTS #{INDEX_NAME};" + else + execute "DROP INDEX IF EXISTS #{INDEX_NAME};" + end end end end diff --git a/db/migrate/20180113220114_rework_redirect_routes_indexes.rb b/db/migrate/20180113220114_rework_redirect_routes_indexes.rb index ab9971be074..53f82a31203 100644 --- a/db/migrate/20180113220114_rework_redirect_routes_indexes.rb +++ b/db/migrate/20180113220114_rework_redirect_routes_indexes.rb @@ -18,51 +18,51 @@ class ReworkRedirectRoutesIndexes < ActiveRecord::Migration OLD_INDEX_NAME_PATH_LOWER = "index_on_redirect_routes_lower_path" def up - disable_statement_timeout - - # this is a plain btree on a single boolean column. It'll never be - # selective enough to be valuable. This class is called by - # setup_postgresql.rake so it needs to be able to handle this - # index not existing. - if index_exists?(:redirect_routes, :permanent) - remove_concurrent_index(:redirect_routes, :permanent) - end + disable_statement_timeout do + # this is a plain btree on a single boolean column. It'll never be + # selective enough to be valuable. This class is called by + # setup_postgresql.rake so it needs to be able to handle this + # index not existing. + if index_exists?(:redirect_routes, :permanent) + remove_concurrent_index(:redirect_routes, :permanent) + end - # If we're on MySQL then the existing index on path is ok. But on - # Postgres we need to clean things up: - return unless Gitlab::Database.postgresql? + # If we're on MySQL then the existing index on path is ok. But on + # Postgres we need to clean things up: + break unless Gitlab::Database.postgresql? - if_not_exists = Gitlab::Database.version.to_f >= 9.5 ? "IF NOT EXISTS" : "" + if_not_exists = Gitlab::Database.version.to_f >= 9.5 ? "IF NOT EXISTS" : "" - # Unique index on lower(path) across both types of redirect_routes: - execute("CREATE UNIQUE INDEX CONCURRENTLY #{if_not_exists} #{INDEX_NAME_UNIQUE} ON redirect_routes (lower(path) varchar_pattern_ops);") + # Unique index on lower(path) across both types of redirect_routes: + execute("CREATE UNIQUE INDEX CONCURRENTLY #{if_not_exists} #{INDEX_NAME_UNIQUE} ON redirect_routes (lower(path) varchar_pattern_ops);") - # Make two indexes on path -- one for permanent and one for temporary routes: - execute("CREATE INDEX CONCURRENTLY #{if_not_exists} #{INDEX_NAME_PERM} ON redirect_routes (lower(path) varchar_pattern_ops) where (permanent);") - execute("CREATE INDEX CONCURRENTLY #{if_not_exists} #{INDEX_NAME_TEMP} ON redirect_routes (lower(path) varchar_pattern_ops) where (not permanent or permanent is null) ;") + # Make two indexes on path -- one for permanent and one for temporary routes: + execute("CREATE INDEX CONCURRENTLY #{if_not_exists} #{INDEX_NAME_PERM} ON redirect_routes (lower(path) varchar_pattern_ops) where (permanent);") + execute("CREATE INDEX CONCURRENTLY #{if_not_exists} #{INDEX_NAME_TEMP} ON redirect_routes (lower(path) varchar_pattern_ops) where (not permanent or permanent is null) ;") - # Remove the old indexes: + # Remove the old indexes: - # This one needed to be on lower(path) but wasn't so it's replaced with the two above - execute "DROP INDEX CONCURRENTLY IF EXISTS #{OLD_INDEX_NAME_PATH_TPOPS};" + # This one needed to be on lower(path) but wasn't so it's replaced with the two above + execute "DROP INDEX CONCURRENTLY IF EXISTS #{OLD_INDEX_NAME_PATH_TPOPS};" - # This one isn't needed because we only ever do = and LIKE on this - # column so the varchar_pattern_ops index is sufficient - execute "DROP INDEX CONCURRENTLY IF EXISTS #{OLD_INDEX_NAME_PATH_LOWER};" + # This one isn't needed because we only ever do = and LIKE on this + # column so the varchar_pattern_ops index is sufficient + execute "DROP INDEX CONCURRENTLY IF EXISTS #{OLD_INDEX_NAME_PATH_LOWER};" + end end def down - disable_statement_timeout + disable_statement_timeout do + add_concurrent_index(:redirect_routes, :permanent) - add_concurrent_index(:redirect_routes, :permanent) + break unless Gitlab::Database.postgresql? - return unless Gitlab::Database.postgresql? + execute("CREATE INDEX CONCURRENTLY #{OLD_INDEX_NAME_PATH_TPOPS} ON redirect_routes (path varchar_pattern_ops);") + execute("CREATE INDEX CONCURRENTLY #{OLD_INDEX_NAME_PATH_LOWER} ON redirect_routes (LOWER(path));") - execute("CREATE INDEX CONCURRENTLY #{OLD_INDEX_NAME_PATH_TPOPS} ON redirect_routes (path varchar_pattern_ops);") - execute("CREATE INDEX CONCURRENTLY #{OLD_INDEX_NAME_PATH_LOWER} ON redirect_routes (LOWER(path));") - - execute("DROP INDEX CONCURRENTLY IF EXISTS #{INDEX_NAME_UNIQUE};") - execute("DROP INDEX CONCURRENTLY IF EXISTS #{INDEX_NAME_PERM};") - execute("DROP INDEX CONCURRENTLY IF EXISTS #{INDEX_NAME_TEMP};") + execute("DROP INDEX CONCURRENTLY IF EXISTS #{INDEX_NAME_UNIQUE};") + execute("DROP INDEX CONCURRENTLY IF EXISTS #{INDEX_NAME_PERM};") + execute("DROP INDEX CONCURRENTLY IF EXISTS #{INDEX_NAME_TEMP};") + end end end diff --git a/db/migrate/20180403035759_create_project_ci_cd_settings.rb b/db/migrate/20180403035759_create_project_ci_cd_settings.rb index 06856af6204..173e662cffc 100644 --- a/db/migrate/20180403035759_create_project_ci_cd_settings.rb +++ b/db/migrate/20180403035759_create_project_ci_cd_settings.rb @@ -13,16 +13,16 @@ class CreateProjectCiCdSettings < ActiveRecord::Migration end end - disable_statement_timeout + disable_statement_timeout do + # This particular INSERT will take between 10 and 20 seconds. + execute 'INSERT INTO project_ci_cd_settings (project_id) SELECT id FROM projects' - # This particular INSERT will take between 10 and 20 seconds. - execute 'INSERT INTO project_ci_cd_settings (project_id) SELECT id FROM projects' + # We add the index and foreign key separately so the above INSERT statement + # takes as little time as possible. + add_concurrent_index(:project_ci_cd_settings, :project_id, unique: true) - # We add the index and foreign key separately so the above INSERT statement - # takes as little time as possible. - add_concurrent_index(:project_ci_cd_settings, :project_id, unique: true) - - add_foreign_key_with_retry + add_foreign_key_with_retry + end end def down diff --git a/db/migrate/20180420010616_cleanup_build_stage_migration.rb b/db/migrate/20180420010616_cleanup_build_stage_migration.rb index 24777294101..5e9fe756efd 100644 --- a/db/migrate/20180420010616_cleanup_build_stage_migration.rb +++ b/db/migrate/20180420010616_cleanup_build_stage_migration.rb @@ -14,48 +14,50 @@ class CleanupBuildStageMigration < ActiveRecord::Migration end def up - disable_statement_timeout - - ## - # We steal from the background migrations queue to catch up with the - # scheduled migrations set. - # - Gitlab::BackgroundMigration.steal('MigrateBuildStage') - - ## - # We add temporary index, to make iteration over batches more performant. - # Conditional here is to avoid the need of doing that in a separate - # migration file to make this operation idempotent. - # - unless index_exists_by_name?(:ci_builds, TMP_INDEX) - add_concurrent_index(:ci_builds, :id, where: 'stage_id IS NULL', name: TMP_INDEX) - end - - ## - # We check if there are remaining rows that should be migrated (for example - # if Sidekiq / Redis fails / is restarted, what could result in not all - # background migrations being executed correctly. - # - # We migrate remaining rows synchronously in a blocking way, to make sure - # that when this migration is done we are confident that all rows are - # already migrated. - # - Build.where('stage_id IS NULL').each_batch(of: 50) do |batch| - range = batch.pluck('MIN(id)', 'MAX(id)').first - - Gitlab::BackgroundMigration::MigrateBuildStage.new.perform(*range) + disable_statement_timeout do + ## + # We steal from the background migrations queue to catch up with the + # scheduled migrations set. + # + Gitlab::BackgroundMigration.steal('MigrateBuildStage') + + ## + # We add temporary index, to make iteration over batches more performant. + # Conditional here is to avoid the need of doing that in a separate + # migration file to make this operation idempotent. + # + unless index_exists_by_name?(:ci_builds, TMP_INDEX) + add_concurrent_index(:ci_builds, :id, where: 'stage_id IS NULL', name: TMP_INDEX) + end + + ## + # We check if there are remaining rows that should be migrated (for example + # if Sidekiq / Redis fails / is restarted, what could result in not all + # background migrations being executed correctly. + # + # We migrate remaining rows synchronously in a blocking way, to make sure + # that when this migration is done we are confident that all rows are + # already migrated. + # + Build.where('stage_id IS NULL').each_batch(of: 50) do |batch| + range = batch.pluck('MIN(id)', 'MAX(id)').first + + Gitlab::BackgroundMigration::MigrateBuildStage.new.perform(*range) + end + + ## + # We remove temporary index, because it is not required during standard + # operations and runtime. + # + remove_concurrent_index_by_name(:ci_builds, TMP_INDEX) end - - ## - # We remove temporary index, because it is not required during standard - # operations and runtime. - # - remove_concurrent_index_by_name(:ci_builds, TMP_INDEX) end def down if index_exists_by_name?(:ci_builds, TMP_INDEX) - remove_concurrent_index_by_name(:ci_builds, TMP_INDEX) + disable_statement_timeout do + remove_concurrent_index_by_name(:ci_builds, TMP_INDEX) + end end end end diff --git a/db/migrate/20180504195842_project_name_lower_index.rb b/db/migrate/20180504195842_project_name_lower_index.rb index d6f25d3d4ab..74f3673bb03 100644 --- a/db/migrate/20180504195842_project_name_lower_index.rb +++ b/db/migrate/20180504195842_project_name_lower_index.rb @@ -13,20 +13,20 @@ class ProjectNameLowerIndex < ActiveRecord::Migration def up return unless Gitlab::Database.postgresql? - disable_statement_timeout - - execute "CREATE INDEX CONCURRENTLY #{INDEX_NAME} ON projects (LOWER(name))" + disable_statement_timeout do + execute "CREATE INDEX CONCURRENTLY #{INDEX_NAME} ON projects (LOWER(name))" + end end def down return unless Gitlab::Database.postgresql? - disable_statement_timeout - - if supports_drop_index_concurrently? - execute "DROP INDEX CONCURRENTLY IF EXISTS #{INDEX_NAME}" - else - execute "DROP INDEX IF EXISTS #{INDEX_NAME}" + disable_statement_timeout do + if supports_drop_index_concurrently? + execute "DROP INDEX CONCURRENTLY IF EXISTS #{INDEX_NAME}" + else + execute "DROP INDEX IF EXISTS #{INDEX_NAME}" + end end end end diff --git a/db/migrate/20180702124358_remove_orphaned_routes.rb b/db/migrate/20180702124358_remove_orphaned_routes.rb index 6f6e289ba87..4068e479b6c 100644 --- a/db/migrate/20180702124358_remove_orphaned_routes.rb +++ b/db/migrate/20180702124358_remove_orphaned_routes.rb @@ -28,16 +28,16 @@ class RemoveOrphanedRoutes < ActiveRecord::Migration # which is pretty close to our 15 second statement timeout. To ensure a # smooth deployment procedure we disable the statement timeouts for this # migration, just in case. - disable_statement_timeout - - # On GitLab.com there are around 4000 orphaned project routes, and around - # 150 orphaned namespace routes. - [ - Route.orphaned_project_routes, - Route.orphaned_namespace_routes - ].each do |relation| - relation.each_batch(of: 1_000) do |batch| - batch.delete_all + disable_statement_timeout do + # On GitLab.com there are around 4000 orphaned project routes, and around + # 150 orphaned namespace routes. + [ + Route.orphaned_project_routes, + Route.orphaned_namespace_routes + ].each do |relation| + relation.each_batch(of: 1_000) do |batch| + batch.delete_all + end end end end diff --git a/db/migrate/20180808162000_add_user_show_add_ssh_key_message_to_application_settings.rb b/db/migrate/20180808162000_add_user_show_add_ssh_key_message_to_application_settings.rb new file mode 100644 index 00000000000..e3019af2cc9 --- /dev/null +++ b/db/migrate/20180808162000_add_user_show_add_ssh_key_message_to_application_settings.rb @@ -0,0 +1,19 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class AddUserShowAddSshKeyMessageToApplicationSettings < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + # Set this constant to true if this migration requires downtime. + DOWNTIME = false + + disable_ddl_transaction! + + def up + add_column_with_default :application_settings, :user_show_add_ssh_key_message, :boolean, default: true, allow_null: false + end + + def down + remove_column :application_settings, :user_show_add_ssh_key_message + end +end diff --git a/db/optional_migrations/composite_primary_keys.rb b/db/optional_migrations/composite_primary_keys.rb index d45705021b0..b330da13d43 100644 --- a/db/optional_migrations/composite_primary_keys.rb +++ b/db/optional_migrations/composite_primary_keys.rb @@ -29,18 +29,20 @@ class CompositePrimaryKeysMigration < ActiveRecord::Migration def up return unless Gitlab::Database.postgresql? - disable_statement_timeout - TABLES.each do |index| - add_primary_key(index) + disable_statement_timeout do + TABLES.each do |index| + add_primary_key(index) + end end end def down return unless Gitlab::Database.postgresql? - disable_statement_timeout - TABLES.each do |index| - remove_primary_key(index) + disable_statement_timeout do + TABLES.each do |index| + remove_primary_key(index) + end end end diff --git a/db/post_migrate/20170502070007_enable_auto_cancel_pending_pipelines_for_all.rb b/db/post_migrate/20170502070007_enable_auto_cancel_pending_pipelines_for_all.rb index bba37e32c01..845c6f0557f 100644 --- a/db/post_migrate/20170502070007_enable_auto_cancel_pending_pipelines_for_all.rb +++ b/db/post_migrate/20170502070007_enable_auto_cancel_pending_pipelines_for_all.rb @@ -8,9 +8,9 @@ class EnableAutoCancelPendingPipelinesForAll < ActiveRecord::Migration DOWNTIME = false def up - disable_statement_timeout - - update_column_in_batches(:projects, :auto_cancel_pending_pipelines, 1) + disable_statement_timeout do + update_column_in_batches(:projects, :auto_cancel_pending_pipelines, 1) + end end def down diff --git a/db/post_migrate/20170503004427_update_retried_for_ci_build.rb b/db/post_migrate/20170503004427_update_retried_for_ci_build.rb index b0b58ab3011..079f0e7511f 100644 --- a/db/post_migrate/20170503004427_update_retried_for_ci_build.rb +++ b/db/post_migrate/20170503004427_update_retried_for_ci_build.rb @@ -7,12 +7,12 @@ class UpdateRetriedForCiBuild < ActiveRecord::Migration disable_ddl_transaction! def up - disable_statement_timeout - if Gitlab::Database.mysql? up_mysql else - up_postgres + disable_statement_timeout do + up_postgres + end end end diff --git a/db/post_migrate/20170508170547_add_head_pipeline_for_each_merge_request.rb b/db/post_migrate/20170508170547_add_head_pipeline_for_each_merge_request.rb index 81e9d050668..5df3ab71648 100644 --- a/db/post_migrate/20170508170547_add_head_pipeline_for_each_merge_request.rb +++ b/db/post_migrate/20170508170547_add_head_pipeline_for_each_merge_request.rb @@ -7,20 +7,20 @@ class AddHeadPipelineForEachMergeRequest < ActiveRecord::Migration disable_ddl_transaction! def up - disable_statement_timeout - pipelines = Arel::Table.new(:ci_pipelines) merge_requests = Arel::Table.new(:merge_requests) - head_id = pipelines - .project(Arel::Nodes::NamedFunction.new('max', [pipelines[:id]])) - .from(pipelines) - .where(pipelines[:ref].eq(merge_requests[:source_branch])) - .where(pipelines[:project_id].eq(merge_requests[:source_project_id])) + disable_statement_timeout do + head_id = pipelines + .project(Arel::Nodes::NamedFunction.new('max', [pipelines[:id]])) + .from(pipelines) + .where(pipelines[:ref].eq(merge_requests[:source_branch])) + .where(pipelines[:project_id].eq(merge_requests[:source_project_id])) - sub_query = Arel::Nodes::SqlLiteral.new(Arel::Nodes::Grouping.new(head_id).to_sql) + sub_query = Arel::Nodes::SqlLiteral.new(Arel::Nodes::Grouping.new(head_id).to_sql) - update_column_in_batches(:merge_requests, :head_pipeline_id, sub_query) + update_column_in_batches(:merge_requests, :head_pipeline_id, sub_query) + end end def down diff --git a/db/post_migrate/20170525140254_rename_all_reserved_paths_again.rb b/db/post_migrate/20170525140254_rename_all_reserved_paths_again.rb index 2125cc046e5..c996ddbec84 100644 --- a/db/post_migrate/20170525140254_rename_all_reserved_paths_again.rb +++ b/db/post_migrate/20170525140254_rename_all_reserved_paths_again.rb @@ -87,16 +87,16 @@ class RenameAllReservedPathsAgain < ActiveRecord::Migration ].freeze def up - disable_statement_timeout - - TOP_LEVEL_ROUTES.each { |route| rename_root_paths(route) } - PROJECT_WILDCARD_ROUTES.each { |route| rename_wildcard_paths(route) } - GROUP_ROUTES.each { |route| rename_child_paths(route) } + disable_statement_timeout do + TOP_LEVEL_ROUTES.each { |route| rename_root_paths(route) } + PROJECT_WILDCARD_ROUTES.each { |route| rename_wildcard_paths(route) } + GROUP_ROUTES.each { |route| rename_child_paths(route) } + end end def down - disable_statement_timeout - - revert_renames + disable_statement_timeout do + revert_renames + end end end diff --git a/db/post_migrate/20170526185842_migrate_pipeline_stages.rb b/db/post_migrate/20170526185842_migrate_pipeline_stages.rb index afd4db183c2..736aff77f02 100644 --- a/db/post_migrate/20170526185842_migrate_pipeline_stages.rb +++ b/db/post_migrate/20170526185842_migrate_pipeline_stages.rb @@ -6,17 +6,17 @@ class MigratePipelineStages < ActiveRecord::Migration disable_ddl_transaction! def up - disable_statement_timeout - - execute <<-SQL.strip_heredoc - INSERT INTO ci_stages (project_id, pipeline_id, name) - SELECT project_id, commit_id, stage FROM ci_builds - WHERE stage IS NOT NULL - AND stage_id IS NULL - AND EXISTS (SELECT 1 FROM projects WHERE projects.id = ci_builds.project_id) - AND EXISTS (SELECT 1 FROM ci_pipelines WHERE ci_pipelines.id = ci_builds.commit_id) - GROUP BY project_id, commit_id, stage - ORDER BY MAX(stage_idx) - SQL + disable_statement_timeout do + execute <<-SQL.strip_heredoc + INSERT INTO ci_stages (project_id, pipeline_id, name) + SELECT project_id, commit_id, stage FROM ci_builds + WHERE stage IS NOT NULL + AND stage_id IS NULL + AND EXISTS (SELECT 1 FROM projects WHERE projects.id = ci_builds.project_id) + AND EXISTS (SELECT 1 FROM ci_pipelines WHERE ci_pipelines.id = ci_builds.commit_id) + GROUP BY project_id, commit_id, stage + ORDER BY MAX(stage_idx) + SQL + end end end diff --git a/db/post_migrate/20170526190000_migrate_build_stage_reference_again.rb b/db/post_migrate/20170526190000_migrate_build_stage_reference_again.rb index 31a73bb3b27..a7bfba0ab2b 100644 --- a/db/post_migrate/20170526190000_migrate_build_stage_reference_again.rb +++ b/db/post_migrate/20170526190000_migrate_build_stage_reference_again.rb @@ -7,22 +7,22 @@ class MigrateBuildStageReferenceAgain < ActiveRecord::Migration disable_ddl_transaction! def up - disable_statement_timeout - stage_id = Arel.sql <<-SQL.strip_heredoc (SELECT id FROM ci_stages WHERE ci_stages.pipeline_id = ci_builds.commit_id AND ci_stages.name = ci_builds.stage) SQL - update_column_in_batches(:ci_builds, :stage_id, stage_id) do |table, query| - query.where(table[:stage_id].eq(nil)) + disable_statement_timeout do + update_column_in_batches(:ci_builds, :stage_id, stage_id) do |table, query| + query.where(table[:stage_id].eq(nil)) + end end end def down - disable_statement_timeout - - update_column_in_batches(:ci_builds, :stage_id, nil) + disable_statement_timeout do + update_column_in_batches(:ci_builds, :stage_id, nil) + end end end diff --git a/db/post_migrate/20170711145558_migrate_stages_statuses.rb b/db/post_migrate/20170711145558_migrate_stages_statuses.rb index 65755c0e824..265f7317b9b 100644 --- a/db/post_migrate/20170711145558_migrate_stages_statuses.rb +++ b/db/post_migrate/20170711145558_migrate_stages_statuses.rb @@ -26,9 +26,9 @@ class MigrateStagesStatuses < ActiveRecord::Migration end def down - disable_statement_timeout - - # rubocop:disable Migration/UpdateLargeTable - update_column_in_batches(:ci_stages, :status, nil) + disable_statement_timeout do + # rubocop:disable Migration/UpdateLargeTable + update_column_in_batches(:ci_stages, :status, nil) + end end end diff --git a/db/post_migrate/20171207150343_remove_soft_removed_objects.rb b/db/post_migrate/20171207150343_remove_soft_removed_objects.rb index 3e2dedfdd6a..3109b6dbf8e 100644 --- a/db/post_migrate/20171207150343_remove_soft_removed_objects.rb +++ b/db/post_migrate/20171207150343_remove_soft_removed_objects.rb @@ -78,12 +78,12 @@ class RemoveSoftRemovedObjects < ActiveRecord::Migration MODELS = [Issue, MergeRequest, CiPipelineSchedule, CiTrigger].freeze def up - disable_statement_timeout - - remove_personal_routes - remove_personal_namespaces - remove_group_namespaces - remove_simple_soft_removed_rows + disable_statement_timeout do + remove_personal_routes + remove_personal_namespaces + remove_group_namespaces + remove_simple_soft_removed_rows + end end def down diff --git a/db/post_migrate/20180119121225_remove_redundant_pipeline_stages.rb b/db/post_migrate/20180119121225_remove_redundant_pipeline_stages.rb index 61ea85eb2a7..269f1287f91 100644 --- a/db/post_migrate/20180119121225_remove_redundant_pipeline_stages.rb +++ b/db/post_migrate/20180119121225_remove_redundant_pipeline_stages.rb @@ -38,29 +38,29 @@ class RemoveRedundantPipelineStages < ActiveRecord::Migration end def remove_redundant_pipeline_stages! - disable_statement_timeout - - redundant_stages_ids = <<~SQL - SELECT id FROM ci_stages WHERE (pipeline_id, name) IN ( - SELECT pipeline_id, name FROM ci_stages - GROUP BY pipeline_id, name HAVING COUNT(*) > 1 - ) - SQL - - execute <<~SQL - UPDATE ci_builds SET stage_id = NULL WHERE stage_id IN (#{redundant_stages_ids}) - SQL - - if Gitlab::Database.postgresql? - execute <<~SQL - DELETE FROM ci_stages WHERE id IN (#{redundant_stages_ids}) + disable_statement_timeout do + redundant_stages_ids = <<~SQL + SELECT id FROM ci_stages WHERE (pipeline_id, name) IN ( + SELECT pipeline_id, name FROM ci_stages + GROUP BY pipeline_id, name HAVING COUNT(*) > 1 + ) SQL - else # We can't modify a table we are selecting from on MySQL + execute <<~SQL - DELETE a FROM ci_stages AS a, ci_stages AS b - WHERE a.pipeline_id = b.pipeline_id AND a.name = b.name - AND a.id <> b.id + UPDATE ci_builds SET stage_id = NULL WHERE stage_id IN (#{redundant_stages_ids}) SQL + + if Gitlab::Database.postgresql? + execute <<~SQL + DELETE FROM ci_stages WHERE id IN (#{redundant_stages_ids}) + SQL + else # We can't modify a table we are selecting from on MySQL + execute <<~SQL + DELETE a FROM ci_stages AS a, ci_stages AS b + WHERE a.pipeline_id = b.pipeline_id AND a.name = b.name + AND a.id <> b.id + SQL + end end end end diff --git a/db/post_migrate/20180305100050_remove_permanent_from_redirect_routes.rb b/db/post_migrate/20180305100050_remove_permanent_from_redirect_routes.rb index db5165dbe70..aa19732ca1c 100644 --- a/db/post_migrate/20180305100050_remove_permanent_from_redirect_routes.rb +++ b/db/post_migrate/20180305100050_remove_permanent_from_redirect_routes.rb @@ -15,10 +15,10 @@ class RemovePermanentFromRedirectRoutes < ActiveRecord::Migration # ReworkRedirectRoutesIndexes: # https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/16211 if Gitlab::Database.postgresql? - disable_statement_timeout - - execute "DROP INDEX CONCURRENTLY IF EXISTS #{INDEX_NAME_PERM};" - execute "DROP INDEX CONCURRENTLY IF EXISTS #{INDEX_NAME_TEMP};" + disable_statement_timeout do + execute "DROP INDEX CONCURRENTLY IF EXISTS #{INDEX_NAME_PERM};" + execute "DROP INDEX CONCURRENTLY IF EXISTS #{INDEX_NAME_TEMP};" + end end remove_column(:redirect_routes, :permanent) @@ -28,10 +28,10 @@ class RemovePermanentFromRedirectRoutes < ActiveRecord::Migration add_column(:redirect_routes, :permanent, :boolean) if Gitlab::Database.postgresql? - disable_statement_timeout - - execute("CREATE INDEX CONCURRENTLY #{INDEX_NAME_PERM} ON redirect_routes (lower(path) varchar_pattern_ops) where (permanent);") - execute("CREATE INDEX CONCURRENTLY #{INDEX_NAME_TEMP} ON redirect_routes (lower(path) varchar_pattern_ops) where (not permanent or permanent is null) ;") + disable_statement_timeout do + execute("CREATE INDEX CONCURRENTLY #{INDEX_NAME_PERM} ON redirect_routes (lower(path) varchar_pattern_ops) where (permanent);") + execute("CREATE INDEX CONCURRENTLY #{INDEX_NAME_TEMP} ON redirect_routes (lower(path) varchar_pattern_ops) where (not permanent or permanent is null) ;") + end end end end diff --git a/db/post_migrate/20180306164012_add_path_index_to_redirect_routes.rb b/db/post_migrate/20180306164012_add_path_index_to_redirect_routes.rb index d6fb4f06695..ca9212fae27 100644 --- a/db/post_migrate/20180306164012_add_path_index_to_redirect_routes.rb +++ b/db/post_migrate/20180306164012_add_path_index_to_redirect_routes.rb @@ -20,10 +20,10 @@ class AddPathIndexToRedirectRoutes < ActiveRecord::Migration def up return unless Gitlab::Database.postgresql? - disable_statement_timeout - - unless index_exists_by_name?(:redirect_routes, INDEX_NAME) - execute("CREATE UNIQUE INDEX CONCURRENTLY #{INDEX_NAME} ON redirect_routes (lower(path) varchar_pattern_ops);") + disable_statement_timeout do + unless index_exists_by_name?(:redirect_routes, INDEX_NAME) + execute("CREATE UNIQUE INDEX CONCURRENTLY #{INDEX_NAME} ON redirect_routes (lower(path) varchar_pattern_ops);") + end end end diff --git a/db/post_migrate/20180405101928_reschedule_builds_stages_migration.rb b/db/post_migrate/20180405101928_reschedule_builds_stages_migration.rb index e19387bce1e..c32123454f9 100644 --- a/db/post_migrate/20180405101928_reschedule_builds_stages_migration.rb +++ b/db/post_migrate/20180405101928_reschedule_builds_stages_migration.rb @@ -17,13 +17,13 @@ class RescheduleBuildsStagesMigration < ActiveRecord::Migration end def up - disable_statement_timeout - - Build.where('stage_id IS NULL').tap do |relation| - queue_background_migration_jobs_by_range_at_intervals(relation, - MIGRATION, - 5.minutes, - batch_size: BATCH_SIZE) + disable_statement_timeout do + Build.where('stage_id IS NULL').tap do |relation| + queue_background_migration_jobs_by_range_at_intervals(relation, + MIGRATION, + 5.minutes, + batch_size: BATCH_SIZE) + end end end diff --git a/db/post_migrate/20180420080616_schedule_stages_index_migration.rb b/db/post_migrate/20180420080616_schedule_stages_index_migration.rb index 1d0daad002f..eb82f098639 100644 --- a/db/post_migrate/20180420080616_schedule_stages_index_migration.rb +++ b/db/post_migrate/20180420080616_schedule_stages_index_migration.rb @@ -13,13 +13,13 @@ class ScheduleStagesIndexMigration < ActiveRecord::Migration end def up - disable_statement_timeout - - Stage.all.tap do |relation| - queue_background_migration_jobs_by_range_at_intervals(relation, - MIGRATION, - 5.minutes, - batch_size: BATCH_SIZE) + disable_statement_timeout do + Stage.all.tap do |relation| + queue_background_migration_jobs_by_range_at_intervals(relation, + MIGRATION, + 5.minutes, + batch_size: BATCH_SIZE) + end end end diff --git a/db/post_migrate/20180604123514_cleanup_stages_position_migration.rb b/db/post_migrate/20180604123514_cleanup_stages_position_migration.rb index 73c23dffca0..5418f442e79 100644 --- a/db/post_migrate/20180604123514_cleanup_stages_position_migration.rb +++ b/db/post_migrate/20180604123514_cleanup_stages_position_migration.rb @@ -12,32 +12,34 @@ class CleanupStagesPositionMigration < ActiveRecord::Migration end def up - disable_statement_timeout + disable_statement_timeout do + Gitlab::BackgroundMigration.steal('MigrateStageIndex') - Gitlab::BackgroundMigration.steal('MigrateStageIndex') - - unless index_exists_by_name?(:ci_stages, TMP_INDEX_NAME) - add_concurrent_index(:ci_stages, :id, where: 'position IS NULL', name: TMP_INDEX_NAME) - end + unless index_exists_by_name?(:ci_stages, TMP_INDEX_NAME) + add_concurrent_index(:ci_stages, :id, where: 'position IS NULL', name: TMP_INDEX_NAME) + end - migratable = <<~SQL - position IS NULL AND EXISTS ( - SELECT 1 FROM ci_builds WHERE stage_id = ci_stages.id AND stage_idx IS NOT NULL - ) - SQL + migratable = <<~SQL + position IS NULL AND EXISTS ( + SELECT 1 FROM ci_builds WHERE stage_id = ci_stages.id AND stage_idx IS NOT NULL + ) + SQL - Stages.where(migratable).each_batch(of: 1000) do |batch| - batch.pluck(:id).each do |stage| - Gitlab::BackgroundMigration::MigrateStageIndex.new.perform(stage, stage) + Stages.where(migratable).each_batch(of: 1000) do |batch| + batch.pluck(:id).each do |stage| + Gitlab::BackgroundMigration::MigrateStageIndex.new.perform(stage, stage) + end end - end - remove_concurrent_index_by_name(:ci_stages, TMP_INDEX_NAME) + remove_concurrent_index_by_name(:ci_stages, TMP_INDEX_NAME) + end end def down if index_exists_by_name?(:ci_stages, TMP_INDEX_NAME) - remove_concurrent_index_by_name(:ci_stages, TMP_INDEX_NAME) + disable_statement_timeout do + remove_concurrent_index_by_name(:ci_stages, TMP_INDEX_NAME) + end end end end diff --git a/db/post_migrate/20180723130817_delete_inconsistent_internal_id_records.rb b/db/post_migrate/20180723130817_delete_inconsistent_internal_id_records.rb new file mode 100644 index 00000000000..3b9b95ec9ca --- /dev/null +++ b/db/post_migrate/20180723130817_delete_inconsistent_internal_id_records.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true +class DeleteInconsistentInternalIdRecords < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + # This migration cleans up any inconsistent records in internal_ids. + # + # That is, it deletes records that track a `last_value` that is + # smaller than the maximum internal id (usually `iid`) found in + # the corresponding model records. + + def up + disable_statement_timeout do + delete_internal_id_records('issues', 'project_id') + delete_internal_id_records('merge_requests', 'project_id', 'target_project_id') + delete_internal_id_records('deployments', 'project_id') + delete_internal_id_records('milestones', 'project_id') + delete_internal_id_records('milestones', 'namespace_id', 'group_id') + delete_internal_id_records('ci_pipelines', 'project_id') + end + end + + class InternalId < ActiveRecord::Base + self.table_name = 'internal_ids' + enum usage: { issues: 0, merge_requests: 1, deployments: 2, milestones: 3, epics: 4, ci_pipelines: 5 } + end + + private + + def delete_internal_id_records(base_table, scope_column_name, base_scope_column_name = scope_column_name) + sql = <<~SQL + SELECT id FROM ( -- workaround for MySQL + SELECT internal_ids.id FROM ( + SELECT #{base_scope_column_name} AS #{scope_column_name}, max(iid) as maximum_iid from #{base_table} GROUP BY #{scope_column_name} + ) maxima JOIN internal_ids USING (#{scope_column_name}) + WHERE internal_ids.usage=#{InternalId.usages.fetch(base_table)} AND maxima.maximum_iid > internal_ids.last_value + ) internal_ids + SQL + + InternalId.where("id IN (#{sql})").tap do |ids| # rubocop:disable GitlabSecurity/SqlInjection + say "Deleting internal_id records for #{base_table}: #{ids.pluck(:project_id, :last_value)}" unless ids.empty? + end.delete_all + end +end diff --git a/db/post_migrate/20180809195358_migrate_null_wiki_access_levels.rb b/db/post_migrate/20180809195358_migrate_null_wiki_access_levels.rb new file mode 100644 index 00000000000..0a0a33299e4 --- /dev/null +++ b/db/post_migrate/20180809195358_migrate_null_wiki_access_levels.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +class MigrateNullWikiAccessLevels < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + class ProjectFeature < ActiveRecord::Base + include EachBatch + + self.table_name = 'project_features' + end + + def up + ProjectFeature.where(wiki_access_level: nil).each_batch do |relation| + relation.update_all(wiki_access_level: 20) + end + + # We need to re-count wikis as previous attempt was not considering the NULLs. + transaction do + execute('SET LOCAL statement_timeout TO 0') if Gitlab::Database.postgresql? # see https://gitlab.com/gitlab-org/gitlab-ce/issues/48967 + + execute("UPDATE site_statistics SET wikis_count = (SELECT COUNT(*) FROM project_features WHERE wiki_access_level != 0)") + end + end + + def down + # there is no way to rollback this change, there are no downsides in keeping migrated data. + end +end diff --git a/db/schema.rb b/db/schema.rb index f1d8f4df3b7..9dc122b54b3 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20180807153545) do +ActiveRecord::Schema.define(version: 20180809195358) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -170,6 +170,7 @@ ActiveRecord::Schema.define(version: 20180807153545) do t.boolean "hide_third_party_offers", default: false, null: false t.boolean "instance_statistics_visibility_private", default: false, null: false t.boolean "web_ide_clientside_preview_enabled", default: false, null: false + t.boolean "user_show_add_ssh_key_message", default: true, null: false end create_table "audit_events", force: :cascade do |t| diff --git a/doc/README.md b/doc/README.md index a814c787f94..4248f62c08c 100644 --- a/doc/README.md +++ b/doc/README.md @@ -133,6 +133,7 @@ scales to run your tests faster. - [GitLab CI/CD](ci/README.md): Explore the features and capabilities of Continuous Integration, Continuous Delivery, and Continuous Deployment with GitLab. - [Review Apps](ci/review_apps/index.md): Preview changes to your app right from a merge request. - [Pipeline Graphs](ci/pipelines.md#pipeline-graphs) +- [JUnit test reports](ci/junit_test_reports.md) ### Package diff --git a/doc/administration/high_availability/gitlab.md b/doc/administration/high_availability/gitlab.md index 637d44d2823..b74554a5598 100644 --- a/doc/administration/high_availability/gitlab.md +++ b/doc/administration/high_availability/gitlab.md @@ -25,11 +25,11 @@ for each GitLab application server in your environment. options. Here is an example snippet to add to `/etc/fstab`: ``` - 10.1.0.1:/var/opt/gitlab/.ssh /var/opt/gitlab/.ssh nfs defaults,soft,rsize=1048576,wsize=1048576,noatime,nofail,lookupcache=positive 0 2 - 10.1.0.1:/var/opt/gitlab/gitlab-rails/uploads /var/opt/gitlab/gitlab-rails/uploads nfs defaults,soft,rsize=1048576,wsize=1048576,noatime,nofail,lookupcache=positive 0 2 - 10.1.0.1:/var/opt/gitlab/gitlab-rails/shared /var/opt/gitlab/gitlab-rails/shared nfs defaults,soft,rsize=1048576,wsize=1048576,noatime,nofail,lookupcache=positive 0 2 - 10.1.0.1:/var/opt/gitlab/gitlab-ci/builds /var/opt/gitlab/gitlab-ci/builds nfs defaults,soft,rsize=1048576,wsize=1048576,noatime,nofail,lookupcache=positive 0 2 - 10.1.0.1:/var/opt/gitlab/git-data /var/opt/gitlab/git-data nfs defaults,soft,rsize=1048576,wsize=1048576,noatime,nofail,lookupcache=positive 0 2 + 10.1.0.1:/var/opt/gitlab/.ssh /var/opt/gitlab/.ssh nfs4 defaults,soft,rsize=1048576,wsize=1048576,noatime,nofail,lookupcache=positive 0 2 + 10.1.0.1:/var/opt/gitlab/gitlab-rails/uploads /var/opt/gitlab/gitlab-rails/uploads nfs4 defaults,soft,rsize=1048576,wsize=1048576,noatime,nofail,lookupcache=positive 0 2 + 10.1.0.1:/var/opt/gitlab/gitlab-rails/shared /var/opt/gitlab/gitlab-rails/shared nfs4 defaults,soft,rsize=1048576,wsize=1048576,noatime,nofail,lookupcache=positive 0 2 + 10.1.0.1:/var/opt/gitlab/gitlab-ci/builds /var/opt/gitlab/gitlab-ci/builds nfs4 defaults,soft,rsize=1048576,wsize=1048576,noatime,nofail,lookupcache=positive 0 2 + 10.1.0.1:/var/opt/gitlab/git-data /var/opt/gitlab/git-data nfs4 defaults,soft,rsize=1048576,wsize=1048576,noatime,nofail,lookupcache=positive 0 2 ``` 1. Create the shared directories. These may be different depending on your NFS diff --git a/doc/administration/high_availability/nfs.md b/doc/administration/high_availability/nfs.md index 387c3fb6a5b..cd2284f5f2a 100644 --- a/doc/administration/high_availability/nfs.md +++ b/doc/administration/high_availability/nfs.md @@ -55,14 +55,14 @@ Below is an example of an NFS mount point defined in `/etc/fstab` we use on GitLab.com: ``` -10.1.1.1:/var/opt/gitlab/git-data /var/opt/gitlab/git-data nfs4 defaults,soft,rsize=1048576,wsize=1048576,noatime,nobootwait,lookupcache=positive 0 2 +10.1.1.1:/var/opt/gitlab/git-data /var/opt/gitlab/git-data nfs4 defaults,soft,rsize=1048576,wsize=1048576,noatime,nofail,lookupcache=positive 0 2 ``` Notice several options that you should consider using: | Setting | Description | | ------- | ----------- | -| `nobootwait` | Don't halt boot process waiting for this mount to become available +| `nofail` | Don't halt boot process waiting for this mount to become available | `lookupcache=positive` | Tells the NFS client to honor `positive` cache results but invalidates any `negative` cache results. Negative cache results cause problems with Git. Specifically, a `git push` can fail to register uniformly across all NFS clients. The negative cache causes the clients to 'remember' that the files did not exist previously. ## A single NFS mount diff --git a/doc/administration/index.md b/doc/administration/index.md index 112d14652af..030a2f95e23 100644 --- a/doc/administration/index.md +++ b/doc/administration/index.md @@ -93,7 +93,6 @@ created in snippets, wikis, and repos. - [Postfix for incoming email](reply_by_email_postfix_setup.md): Set up a basic Postfix mail server with IMAP authentication on Ubuntu for incoming emails. -- [User Cohorts](../user/admin_area/user_cohorts.md): Display the monthly cohorts of new users and their activities over time. [reply by email]: reply_by_email.md [issues by email]: ../user/project/issues/create_new_issue.md#new-issue-via-email @@ -137,7 +136,6 @@ created in snippets, wikis, and repos. - [Monitoring uptime](../user/admin_area/monitoring/health_check.md): Check the server status using the health check endpoint. - [IP whitelist](monitoring/ip_whitelist.md): Monitor endpoints that provide health check information when probed. - [Monitoring GitHub imports](monitoring/github_imports.md): GitLab's GitHub Importer displays Prometheus metrics to monitor the health and progress of the importer. -- [Conversational Development (ConvDev) Index](../user/admin_area/monitoring/convdev.md): Provides an overview of your entire instance's feature usage. ### Performance Monitoring diff --git a/doc/administration/job_traces.md b/doc/administration/job_traces.md index 24d1a3fd151..6e2f67f61bc 100644 --- a/doc/administration/job_traces.md +++ b/doc/administration/job_traces.md @@ -3,10 +3,6 @@ Job traces are sent by GitLab Runner while it's processing a job. You can see traces in job pages, pipelines, email notifications, etc. -There isn't a way to automatically expire old job logs, but it's safe to remove -them if they're taking up too much space. If you remove the logs manually, the -job output in the UI will be empty. - ## Data flow In general, there are two states in job traces: "live trace" and "archived trace". @@ -57,11 +53,55 @@ To change the location where the job logs will be stored, follow the steps below ## Uploading traces to object storage -An archived trace is considered as a [job artifact](job_artifacts.md). -Therefore, when you [set up an object storage](job_artifacts.md#object-storage-settings), +Archived traces are considered as [job artifacts](job_artifacts.md). +Therefore, when you [set up the object storage integration](job_artifacts.md#object-storage-settings), job traces are automatically migrated to it along with the other job artifacts. -See [Data flow](#data-flow) to learn about the process. +See "Phase 4: uploading" in [Data flow](#data-flow) to learn about the process. + +## How to archive legacy job trace files + +Legacy job traces, which were created before GitLab 10.5, were not archived regularly. +It's the same state with the "2: overwriting" in the above [Data flow](#data-flow). +To archive those legacy job traces, please follow the instruction below. + +1. Execute the following command + + ```bash + gitlab-rake gitlab:traces:archive + ``` + + After you executed this task, GitLab instance queues up Sidekiq jobs (asynchronous processes) + for migrating job trace files from local storage to object storage. + It could take time to complete the all migration jobs. You can check the progress by the following command + + ```bash + sudo gitlab-rails console + ``` + + ```bash + [1] pry(main)> Sidekiq::Stats.new.queues['pipeline_background:archive_trace'] + => 100 + ``` + + If the count becomes zero, the archiving processes are done + +## How to migrate archived job traces to object storage + +If job traces have already been archived into local storage, and you want to migrate those traces to object storage, please follow the instruction below. + +1. Ensure [Object storage integration for Job Artifacts](job_artifacts.md#object-storage-settings) is enabled +1. Execute the following command + + ```bash + gitlab-rake gitlab:traces:migrate + ``` + +## How to remove job traces + +There isn't a way to automatically expire old job logs, but it's safe to remove +them if they're taking up too much space. If you remove the logs manually, the +job output in the UI will be empty. ## New live trace architecture diff --git a/doc/administration/operations/ssh_certificates.md b/doc/administration/operations/ssh_certificates.md index 8968afba01b..9edccd25ced 100644 --- a/doc/administration/operations/ssh_certificates.md +++ b/doc/administration/operations/ssh_certificates.md @@ -163,3 +163,20 @@ Such a restriction can currently be hacked in by e.g. providing a custom `AuthorizedKeysCommand` which checks if the discovered key-ID returned from `gitlab-shell-authorized-keys-check` is a deploy key or not (all non-deploy keys should be refused). + +## Disabling the global warning about users lacking SSH keys + +By default GitLab will show a "You won't be able to pull or push +project code via SSH" warning to users who have not uploaded an SSH +key to their profile. + +This is counterproductive when using SSH certificates, since users +aren't expected to upload their own keys. + +To disable this warning globally, go to "Application settings -> +Account and limit settings" and disable the "Show user add SSH key +message" setting. + +This setting was added specifically for use with SSH certificates, but +can be turned off without using them if you'd like to hide the warning +for some other reason. diff --git a/doc/administration/repository_storage_types.md b/doc/administration/repository_storage_types.md index 88221db78f1..bd758c49eba 100644 --- a/doc/administration/repository_storage_types.md +++ b/doc/administration/repository_storage_types.md @@ -42,7 +42,7 @@ Registry, etc. ## Hashed Storage > **Warning:** Hashed storage is in **Beta**. For the latest updates, check the -> associated [issue](https://gitlab.com/gitlab-com/infrastructure/issues/2821) +> associated [issue](https://gitlab.com/gitlab-com/infrastructure/issues/3542) > and please report any problems you encounter. Hashed Storage is the new storage behavior we are rolling out with 10.0. Instead diff --git a/doc/api/boards.md b/doc/api/boards.md index 246de50323e..5f006f4f012 100644 --- a/doc/api/boards.md +++ b/doc/api/boards.md @@ -144,7 +144,7 @@ Example response: ## List board lists Get a list of the board's lists. -Does not include `backlog` and `closed` lists +Does not include `open` and `closed` lists ``` GET /projects/:id/boards/:board_id/lists diff --git a/doc/api/events.md b/doc/api/events.md index f4d26c4de1c..1b6c4d437dd 100644 --- a/doc/api/events.md +++ b/doc/api/events.md @@ -255,7 +255,7 @@ Example response: Get a list of visible events for a particular project. ``` -GET /:project_id/events +GET /projects/:project_id/events ``` Parameters: diff --git a/doc/api/group_boards.md b/doc/api/group_boards.md index 45a8544d6b1..373904e50c4 100644 --- a/doc/api/group_boards.md +++ b/doc/api/group_boards.md @@ -119,7 +119,7 @@ Example response: ## List board lists Get a list of the board's lists. -Does not include `backlog` and `closed` lists +Does not include `open` and `closed` lists ``` GET /groups/:id/boards/:board_id/lists diff --git a/doc/api/jobs.md b/doc/api/jobs.md index 9a950097675..4bf65a8fafd 100644 --- a/doc/api/jobs.md +++ b/doc/api/jobs.md @@ -33,7 +33,6 @@ Example of response }, "coverage": null, "created_at": "2015-12-24T15:51:21.727Z", - "artifacts_file": null, "finished_at": "2015-12-24T17:54:24.921Z", "artifacts_expire_at": "2016-01-23T17:54:24.921Z", "id": 6, @@ -45,6 +44,7 @@ Example of response "status": "pending" }, "ref": "master", + "artifacts": [], "runner": null, "stage": "test", "started_at": "2015-12-24T17:54:24.729Z", @@ -82,6 +82,12 @@ Example of response "filename": "artifacts.zip", "size": 1000 }, + "artifacts": [ + {"file_type": "archive", "size": 1000, "filename": "artifacts.zip", "file_format": "zip"}, + {"file_type": "metadata", "size": 186, "filename": "metadata.gz", "file_format": "gzip"}, + {"file_type": "trace", "size": 1500, "filename": "job.log", "file_format": "raw"}, + {"file_type": "junit", "size": 750, "filename": "junit.xml.gz", "file_format": "gzip"} + ], "finished_at": "2015-12-24T17:54:27.895Z", "artifacts_expire_at": "2016-01-23T17:54:27.895Z", "id": 7, @@ -93,6 +99,7 @@ Example of response "status": "pending" }, "ref": "master", + "artifacts": [], "runner": null, "stage": "test", "started_at": "2015-12-24T17:54:27.722Z", @@ -151,7 +158,6 @@ Example of response }, "coverage": null, "created_at": "2015-12-24T15:51:21.727Z", - "artifacts_file": null, "finished_at": "2015-12-24T17:54:24.921Z", "artifacts_expire_at": "2016-01-23T17:54:24.921Z", "id": 6, @@ -163,6 +169,7 @@ Example of response "status": "pending" }, "ref": "master", + "artifacts": [], "runner": null, "stage": "test", "started_at": "2015-12-24T17:54:24.729Z", @@ -200,6 +207,12 @@ Example of response "filename": "artifacts.zip", "size": 1000 }, + "artifacts": [ + {"file_type": "archive", "size": 1000, "filename": "artifacts.zip", "file_format": "zip"}, + {"file_type": "metadata", "size": 186, "filename": "metadata.gz", "file_format": "gzip"}, + {"file_type": "trace", "size": 1500, "filename": "job.log", "file_format": "raw"}, + {"file_type": "junit", "size": 750, "filename": "junit.xml.gz", "file_format": "gzip"} + ], "finished_at": "2015-12-24T17:54:27.895Z", "artifacts_expire_at": "2016-01-23T17:54:27.895Z", "id": 7, @@ -211,6 +224,7 @@ Example of response "status": "pending" }, "ref": "master", + "artifacts": [], "runner": null, "stage": "test", "started_at": "2015-12-24T17:54:27.722Z", @@ -267,7 +281,6 @@ Example of response }, "coverage": null, "created_at": "2015-12-24T15:51:21.880Z", - "artifacts_file": null, "finished_at": "2015-12-24T17:54:31.198Z", "artifacts_expire_at": "2016-01-23T17:54:31.198Z", "id": 8, @@ -279,6 +292,7 @@ Example of response "status": "pending" }, "ref": "master", + "artifacts": [], "runner": null, "stage": "test", "started_at": "2015-12-24T17:54:30.733Z", @@ -458,11 +472,11 @@ Example of response }, "coverage": null, "created_at": "2016-01-11T10:13:33.506Z", - "artifacts_file": null, "finished_at": "2016-01-11T10:14:09.526Z", "id": 42, "name": "rubocop", "ref": "master", + "artifacts": [], "runner": null, "stage": "test", "started_at": null, @@ -505,11 +519,11 @@ Example of response }, "coverage": null, "created_at": "2016-01-11T10:13:33.506Z", - "artifacts_file": null, "finished_at": null, "id": 42, "name": "rubocop", "ref": "master", + "artifacts": [], "runner": null, "stage": "test", "started_at": null, @@ -559,6 +573,7 @@ Example of response "id": 42, "name": "rubocop", "ref": "master", + "artifacts": [], "runner": null, "stage": "test", "created_at": "2016-01-11T10:13:33.506Z", @@ -610,6 +625,7 @@ Example response: "id": 42, "name": "rubocop", "ref": "master", + "artifacts": [], "runner": null, "stage": "test", "created_at": "2016-01-11T10:13:33.506Z", @@ -654,11 +670,11 @@ Example of response }, "coverage": null, "created_at": "2016-01-11T10:13:33.506Z", - "artifacts_file": null, "finished_at": null, "id": 42, "name": "rubocop", "ref": "master", + "artifacts": [], "runner": null, "stage": "test", "started_at": null, diff --git a/doc/api/notification_settings.md b/doc/api/notification_settings.md index 682b90361bd..165b9a11c7a 100644 --- a/doc/api/notification_settings.md +++ b/doc/api/notification_settings.md @@ -15,7 +15,7 @@ mention custom ``` -If the `custom` level is used, specific email events can be controlled. Notification email events are defined in the `NotificationSetting::EMAIL_EVENTS` model variable. Currently, these events are recognized: +If the `custom` level is used, specific email events can be controlled. Available events are returned by `NotificationSetting.email_events`. Currently, these events are recognized: ``` new_note diff --git a/doc/api/projects.md b/doc/api/projects.md index f360b49c293..bda4164ee92 100644 --- a/doc/api/projects.md +++ b/doc/api/projects.md @@ -258,8 +258,7 @@ GET /projects?custom_attributes[key]=value&custom_attributes[other_key]=other_va ## List user projects -Get a list of visible projects for the given user. When accessed without -authentication, only public projects are returned. +Get a list of visible projects owned by the given user. When accessed without authentication, only public projects are returned. ``` GET /users/:user_id/projects diff --git a/doc/api/repositories.md b/doc/api/repositories.md index cb816bbd712..a4fdeca162e 100644 --- a/doc/api/repositories.md +++ b/doc/api/repositories.md @@ -204,3 +204,39 @@ Response: "deletions": 244 }] ``` + +## Merge Base + +Get the common ancestor for 2 refs (commit SHAs, branch names or tags). + +``` +GET /projects/:id/repository/merge_base +``` + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) | +| `refs` | array | yes | The refs to find the common ancestor of, for now only 2 refs are supported | + +```bash +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/5/repository/merge_base?refs[]=304d257dcb821665ab5110318fc58a007bd104ed&refs[]=0031876facac3f2b2702a0e53a26e89939a42209" +``` + +Example response: + +```json +{ + "id": "1a0b36b3cdad1d2ee32457c102a8c0b7056fa863", + "short_id": "1a0b36b3", + "title": "Initial commit", + "created_at": "2014-02-27T08:03:18.000Z", + "parent_ids": [], + "message": "Initial commit\n", + "author_name": "Dmitriy Zaporozhets", + "author_email": "dmitriy.zaporozhets@gmail.com", + "authored_date": "2014-02-27T08:03:18.000Z", + "committer_name": "Dmitriy Zaporozhets", + "committer_email": "dmitriy.zaporozhets@gmail.com", + "committed_date": "2014-02-27T08:03:18.000Z" +} +``` diff --git a/doc/api/services.md b/doc/api/services.md index efa173180bb..8c59b232b6d 100644 --- a/doc/api/services.md +++ b/doc/api/services.md @@ -401,48 +401,6 @@ Get Flowdock service settings for a project. GET /projects/:id/services/flowdock ``` -## Gemnasium - -Gemnasium monitors your project dependencies and alerts you about updates and security vulnerabilities. - -CAUTION: **Warning:** -Gemnasium service integration has been deprecated in GitLab 11.0. Gemnasium has been -[acquired by GitLab](https://about.gitlab.com/press/releases/2018-01-30-gemnasium-acquisition.html) -in January 2018 and since May 15, 2018, the service provided by Gemnasium is no longer available. -You can [migrate from Gemnasium to GitLab](https://docs.gitlab.com/ee/user/project/import/gemnasium.html) -to keep monitoring your dependencies. - -### Create/Edit Gemnasium service - -Set Gemnasium service for a project. - -``` -PUT /projects/:id/services/gemnasium -``` - -Parameters: - -| Parameter | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `api_key` | string | true | Your personal API KEY on gemnasium.com | -| `token` | string | true | The project's slug on gemnasium.com | - -### Delete Gemnasium service - -Delete Gemnasium service for a project. - -``` -DELETE /projects/:id/services/gemnasium -``` - -### Get Gemnasium service settings - -Get Gemnasium service settings for a project. - -``` -GET /projects/:id/services/gemnasium -``` - ## Hangouts Chat Google GSuite team collaboration tool. diff --git a/doc/api/settings.md b/doc/api/settings.md index 68fc56b1fa3..b480d62e16a 100644 --- a/doc/api/settings.md +++ b/doc/api/settings.md @@ -56,7 +56,8 @@ Example response: "enforce_terms": true, "terms": "Hello world!", "performance_bar_allowed_group_id": 42, - "instance_statistics_visibility_private": false + "instance_statistics_visibility_private": false, + "user_show_add_ssh_key_message": true } ``` @@ -161,6 +162,8 @@ PUT /application/settings | `enforce_terms` | boolean | no | Enforce application ToS to all users | | `terms` | text | yes (if `enforce_terms` is true) | Markdown content for the ToS | | `instance_statistics_visibility_private` | boolean | no | When set to `true` Instance statistics will only be available to admins | +| `user_show_add_ssh_key_message` | boolean | no | When set to `false` disable the "You won't be able to pull or push ++project code via SSH" warning shown to users with no uploaded SSH key | ```bash curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/application/settings?signup_enabled=false&default_project_visibility=internal @@ -206,6 +209,7 @@ Example response: "enforce_terms": true, "terms": "Hello world!", "performance_bar_allowed_group_id": 42, - "instance_statistics_visibility_private": false + "instance_statistics_visibility_private": false, + "user_show_add_ssh_key_message": true } ``` diff --git a/doc/api/system_hooks.md b/doc/api/system_hooks.md index dd424470b67..7b8db6cfa8f 100644 --- a/doc/api/system_hooks.md +++ b/doc/api/system_hooks.md @@ -34,6 +34,7 @@ Example response: "push_events":true, "tag_push_events":false, "merge_requests_events": true, + "repository_update_events": true, "enable_ssl_verification":true } ] @@ -56,6 +57,7 @@ POST /hooks | `push_events` | boolean | no | When true, the hook will fire on push events | | `tag_push_events` | boolean | no | When true, the hook will fire on new tags being pushed | | `merge_requests_events` | boolean | no | Trigger hook on merge requests events | +| `repository_update_events` | boolean | no | Trigger hook on repository update events | | `enable_ssl_verification` | boolean | no | Do SSL verification when triggering the hook | Example request: @@ -75,6 +77,7 @@ Example response: "push_events":true, "tag_push_events":false, "merge_requests_events": true, + "repository_update_events": true, "enable_ssl_verification":true } ] @@ -127,4 +130,4 @@ Example request: ```bash curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/hooks/2 -``` +```
\ No newline at end of file diff --git a/doc/ci/README.md b/doc/ci/README.md index 7666219acb0..d782d64e971 100644 --- a/doc/ci/README.md +++ b/doc/ci/README.md @@ -76,6 +76,8 @@ learn how to leverage its potential even more. - [Trigger pipelines on a schedule](../user/project/pipelines/schedules.md) - [Kubernetes clusters](../user/project/clusters/index.md) - Integrate one or more Kubernetes clusters to your project +- [Interactive web terminal](interactive_web_terminal/index.md) - Open an interactive + web terminal to debug the running jobs ## GitLab CI/CD for Docker diff --git a/doc/ci/docker/README.md b/doc/ci/docker/README.md index b0e01d74f7e..8ae80b2bc02 100644 --- a/doc/ci/docker/README.md +++ b/doc/ci/docker/README.md @@ -6,3 +6,4 @@ comments: false - [Using Docker Images](using_docker_images.md) - [Using Docker Build](using_docker_build.md) +- [Using kaniko](using_kaniko.md) diff --git a/doc/ci/docker/using_kaniko.md b/doc/ci/docker/using_kaniko.md new file mode 100644 index 00000000000..7d4f28e1f47 --- /dev/null +++ b/doc/ci/docker/using_kaniko.md @@ -0,0 +1,60 @@ +# Building images with kaniko and GitLab CI/CD + +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/45512) in GitLab 11.2. +Requires GitLab Runner 11.2 and above. + +[kaniko](https://github.com/GoogleContainerTools/kaniko) is a tool to build +container images from a Dockerfile, inside a container or Kubernetes cluster. + +kaniko solves two problems with using the +[docker-in-docker build](using_docker_build.md#use-docker-in-docker-executor) method: + +1. Docker-in-docker requires [privileged mode](https://docs.docker.com/engine/reference/run/#runtime-privilege-and-linux-capabilities) + in order to function, which is a significant security concern. +1. Docker-in-docker generally incurs a performance penalty and can be quite slow. + +## Requirements + +In order to utilize kaniko with GitLab, a [GitLab Runner](https://docs.gitlab.com/runner/) +using either the [Kubernetes](https://docs.gitlab.com/runner/executors/kubernetes.html), +[Docker](https://docs.gitlab.com/runner/executors/docker.html), or +[Docker Machine](https://docs.gitlab.com/runner/executors/docker_machine.html) +executors is required. + +## Building a Docker image with kaniko + +When building an image with kaniko and GitLab CI/CD, you should be aware of a +few important details: + +- The kaniko debug image is recommended (`gcr.io/kaniko-project/executor:debug`) + because it has a shell, and a shell is required for an image to be used with + GitLab CI/CD. +- The entrypoint will need to be [overridden](using_docker_images.md#overriding-the-entrypoint-of-an-image), + otherwise the build script will not run. +- A Docker `config.json` file needs to be created with the authentication + information for the desired container registry. + +--- + +In the following example, kaniko is used to build a Docker image and then push +it to [GitLab Container Registry](../../user/project/container_registry.md). +The job will run only when a tag is pushed. A `config.json` file is created under +`/root/.docker` with the needed GitLab Container Registry credentials taken from the +[environment variables](../variables/README.md#predefined-variables-environment-variables) +GitLab CI/CD provides. In the last step, kaniko uses the `Dockerfile` under the +root directory of the project, builds the Docker image and pushes it to the +project's Container Registry while tagging it with the Git tag: + +```yaml +build: + stage: build + image: + name: gcr.io/kaniko-project/executor:debug + entrypoint: [""] + script: + - mkdir -p /root/.docker + - echo "{\"auths\":{\"$CI_REGISTRY\":{\"username\":\"$CI_REGISTRY_USER\",\"password\":\"$CI_REGISTRY_PASSWORD\"}}}" > /root/.docker/config.json + - /kaniko/executor --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/Dockerfile --destination $CI_REGISTRY_IMAGE:$CI_COMMIT_TAG + only: + - tags +``` diff --git a/doc/ci/examples/README.md b/doc/ci/examples/README.md index 811f4d1f07a..8eb96ae10b2 100644 --- a/doc/ci/examples/README.md +++ b/doc/ci/examples/README.md @@ -43,6 +43,10 @@ There's also a collection of repositories with [example projects](https://gitlab - [Using `dpl` as deployment tool](deployment/README.md) - [The `.gitlab-ci.yml` file for GitLab itself](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/.gitlab-ci.yml) +## Test Reports + +[Collect test reports in Verify stage](../junit_test_reports.md). + ## Code Quality analysis **(Starter)** [Analyze your project's Code Quality](code_quality.md). diff --git a/doc/ci/img/junit_test_report.png b/doc/ci/img/junit_test_report.png Binary files differnew file mode 100644 index 00000000000..ad098eb457f --- /dev/null +++ b/doc/ci/img/junit_test_report.png diff --git a/doc/ci/interactive_web_terminal/img/finished_job_with_terminal_open.png b/doc/ci/interactive_web_terminal/img/finished_job_with_terminal_open.png Binary files differnew file mode 100644 index 00000000000..199268a1486 --- /dev/null +++ b/doc/ci/interactive_web_terminal/img/finished_job_with_terminal_open.png diff --git a/doc/ci/interactive_web_terminal/img/interactive_web_terminal_page.png b/doc/ci/interactive_web_terminal/img/interactive_web_terminal_page.png Binary files differnew file mode 100644 index 00000000000..b59c1b6bc43 --- /dev/null +++ b/doc/ci/interactive_web_terminal/img/interactive_web_terminal_page.png diff --git a/doc/ci/interactive_web_terminal/img/interactive_web_terminal_running_job.png b/doc/ci/interactive_web_terminal/img/interactive_web_terminal_running_job.png Binary files differnew file mode 100644 index 00000000000..f92c6df07a1 --- /dev/null +++ b/doc/ci/interactive_web_terminal/img/interactive_web_terminal_running_job.png diff --git a/doc/ci/interactive_web_terminal/index.md b/doc/ci/interactive_web_terminal/index.md new file mode 100644 index 00000000000..507aceb27fa --- /dev/null +++ b/doc/ci/interactive_web_terminal/index.md @@ -0,0 +1,52 @@ +# Getting started with interactive web terminals + +> Introduced in GitLab 11.3. + +CAUTION: **Warning:** +Interactive web terminals are in beta, so they might not work properly and +lack features. For more information [follow issue #25990](https://gitlab.com/gitlab-org/gitlab-ce/issues/25990). + +Interactive web terminals give the user access to a terminal in GitLab for +running one-of commands for their CI pipeline. + +NOTE: **Note:** +This is not available for the shared Runners on GitLab.com. +To make use of this feature, you need to provide your +[own Runner](https://docs.gitlab.com/runner/install/) and properly +[configure it](#configuration). + +## Configuration + +Two things need to be configured for the interactive web terminal to work: + +- The Runner needs to have [`[session_server]` configured + properly][session-server] +- Web terminals need to be + [enabled](../../administration/integration/terminal.md#enabling-and-disabling-terminal-support) + +## Debugging a running job + +NOTE: **Note:** Not all executors are +[supported](https://docs.gitlab.com/runner/executors/#compatibility-chart). + +Sometimes, when a job is running, things don't go as you would expect, and it +would be helpful if one can have a shell to aid debugging. When a job is +running, on the right panel you can see a button `debug` that will open the terminal +for the current job. + +![Example of job running with terminal +available](img/interactive_web_terminal_running_job.png) + +When clicked, a new tab will open to the terminal page where you can access +the terminal and type commands like a normal shell. + +![terminal of the job](img/interactive_web_terminal_page.png) + +If you have the terminal open and the job has finished with its tasks, the +terminal will block the job from finishing for the duration configured in +[`[session_server].terminal_max_retention_time`][session-server] until you +close the terminal window. + +![finished job with terminal open](img/finished_job_with_terminal_open.png) + +[session-server]: https://docs.gitlab.com/runner/configuration/advanced-configuration.html#the-session_server-section diff --git a/doc/ci/junit_test_reports.md b/doc/ci/junit_test_reports.md new file mode 100644 index 00000000000..5ae8ecaafa6 --- /dev/null +++ b/doc/ci/junit_test_reports.md @@ -0,0 +1,102 @@ +# JUnit test reports + +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/45318) in GitLab 11.2. +Requires GitLab Runner 11.2 and above. + +## Overview + +It is very common that a [CI/CD pipeline](pipelines.md) contains a +test job that will verify your code. +If the tests fail, the pipeline fails and users get notified. The person that +works on the merge request will have to check the job logs and see where the +tests failed so that they can fix them. + +You can configure your job to use JUnit test reports, and GitLab will display a +report on the merge request so that it's easier and faster to identify the +failure without having to check the entire log. + +## Use cases + +Consider the following workflow: + +1. Your `master` branch is rock solid, your project is using GitLab CI/CD and + your pipelines indicate that there isn't anything broken. +1. Someone from you team submits a merge request, a test fails and the pipeline + gets the known red icon. To investigate more, you have to go through the job + logs to figure out the cause of the failed test, which usually contain + thousands of lines. +1. You configure the JUnit test reports and immediately GitLab collects and + exposes them in the merge request. No more searching in the job logs. +1. Your development and debugging workflow becomes easier, faster and efficient. + +## How it works + +First, GitLab Runner uploads all JUnit XML files as artifacts to GitLab. Then, +when you visit a merge request, GitLab starts comparing the head and base branch's +JUnit test reports, where: + +- The base branch is the target branch (usually `master`). +- The head branch is the source branch (the latest pipeline in each merge request). + +The reports panel has a summary showing how many tests failed and how many were fixed. +If no comparison can be done because data for the base branch is not available, +the panel will just show the list of failed tests for head. + +There are three types of results: + +1. **Newly failed tests:** Test cases which passed on base branch and failed on head branch +1. **Existing failures:** Test cases which failed on base branch and failed on head branch +1. **Resolved failures:** Test cases which failed on base branch and passed on head branch + +Each entry in the panel will show the test name and its type from the list +above. Clicking on the test name will open a modal window with details of its +execution time and the error output. + +![Test Reports Widget](img/junit_test_report.png) + +## How to set it up + +NOTE: **Note:** +For a list of supported languages on JUnit tests, check the +[Wikipedia article](https://en.wikipedia.org/wiki/JUnit#Ports). + +To enable the JUnit reports in merge requests, you need to add +[`artifacts:reports:junit`](yaml/README.md#artifacts-reports-junit) +in `.gitlab-ci.yml`, and specify the path(s) of the generated test reports. + +In the following examples, the job in the `test` stage runs and GitLab +collects the JUnit test report from each job. After each job is executed, the +XML reports are stored in GitLab as artifacts and their results are shown in the +merge request widget. + +### Ruby example + +Use the following job in `.gitlab-ci.yml`: + +```yaml +## Use https://github.com/sj26/rspec_junit_formatter to generate a JUnit report with rspec +ruby: + stage: test + script: + - bundle install + - rspec spec/lib/ --format RspecJunitFormatter --out rspec.xml + artifacts: + reports: + junit: rspec.xml +``` + +### Go example + +Use the following job in `.gitlab-ci.yml`: + +```yaml +## Use https://github.com/jstemmer/go-junit-report to generate a JUnit report with go +golang: + stage: test + script: + - go get -u github.com/jstemmer/go-junit-report + - go test -v 2>&1 | go-junit-report > report.xml + artifacts: + reports: + junit: report.xml +``` diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index 95d705d3a3d..abba748db8b 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -1075,8 +1075,10 @@ keep artifacts forever. After their expiry, artifacts are deleted hourly by default (via a cron job), and are not accessible anymore. -The value of `expire_in` is an elapsed time. Examples of parsable values: +The value of `expire_in` is an elapsed time in seconds, unless a unit is +provided. Examples of parsable values: +- '42' - '3 mins 4 sec' - '2 hrs 20 min' - '2h20min' @@ -1092,6 +1094,52 @@ job: expire_in: 1 week ``` +### `artifacts:reports` + +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/20390) in +GitLab 11.2. Requires GitLab Runner 11.2 and above. + +The `reports` keyword is used for collecting test reports from jobs and +exposing them in GitLab's UI (merge requests, pipeline views). Read how to use +this with [JUnit reports](#artifacts-reports-junit). + +NOTE: **Note:** +The test reports are collected regardless of the job results (success or failure). +You can use [`artifacts:expire_in`](#artifacts-expire_in) to set up an expiration +date for their artifacts. + +#### `artifacts:reports:junit` + +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/20390) in +GitLab 11.2. Requires GitLab Runner 11.2 and above. + +The `junit` report collects [JUnit XML files](https://www.ibm.com/support/knowledgecenter/en/SSQ2R2_14.1.0/com.ibm.rsar.analysis.codereview.cobol.doc/topics/cac_useresults_junit.html) +as artifacts. Although JUnit was originally developed in Java, there are many +[third party ports](https://en.wikipedia.org/wiki/JUnit#Ports) for other +languages like Javascript, Python, Ruby, etc. + +Below is an example of collecting a JUnit XML file from Ruby's RSpec test tool: + +```yaml +rspec: + stage: test + script: + - bundle install + - rspec --format RspecJunitFormatter --out rspec.xml + artifacts: + reports: + junit: rspec.xml +``` + +The collected JUnit reports will be uploaded to GitLab as an artifact and will +be automatically [shown in merge requests](../junit_test_reports.md). + +NOTE: **Note:** +In case the JUnit tool you use exports to multiple XML files, you can specify +multiple test report paths within a single job +(`junit: [rspec-1.xml, rspec-2.xml, rspec-3.xml]`) and they will be automatically +concatenated into a single file. + ## `dependencies` > Introduced in GitLab 8.6 and GitLab Runner v1.1.1. diff --git a/doc/development/README.md b/doc/development/README.md index fed3903c771..ee9a9852205 100644 --- a/doc/development/README.md +++ b/doc/development/README.md @@ -55,7 +55,13 @@ description: 'Learn how to contribute to GitLab.' - [Merge request performance guidelines](merge_request_performance_guidelines.md) for ensuring merge requests do not negatively impact GitLab performance -## Databases guides +## Database guides + +### Tooling + +- [Understanding EXPLAIN plans](understanding_explain_plans.md) +- [explain.depesz.com](https://explain.depesz.com/) for visualising the output + of `EXPLAIN` ### Migrations diff --git a/doc/development/contributing/design.md b/doc/development/contributing/design.md index d4cce79b067..57ae318e821 100644 --- a/doc/development/contributing/design.md +++ b/doc/development/contributing/design.md @@ -18,23 +18,16 @@ To better understand the priority by which UX tackles issues, see the [UX sectio Once an issue has been worked on and is ready for development, a UXer removes the ~"UX" label and applies the ~"UX ready" label to that issue. -The UX team has a special type label called ~"design artifact". This label indicates that the final output -for an issue is a UX solution/design. The solution will be developed by frontend and/or backend in a subsequent milestone. -Any issue labeled ~"design artifact" should not also be labeled ~"frontend" or ~"backend" since no development is -needed until the solution has been decided. +There is a special type label called ~"product discovery". It represents a discovery issue intended for UX, PM, FE, and BE to discuss the problem and potential solutions. The final output for this issue could be a doc of requirements, a design artifact, or even a prototype. The solution will be developed in a subsequent milestone. -~"design artifact" issues are like any other issue and should contain a milestone label, ~"Deliverable" or ~"Stretch", when scheduled in the current milestone. +~"product discovery" issues are like any other issue and should contain a milestone label, ~"Deliverable" or ~"Stretch", when scheduled in the current milestone. -To prevent the misunderstanding that a feature will be be delivered in the -assigned milestone, when only UX design is planned for that milestone, the -Product Manager should create a separate issue for the ~"design artifact", -assign the ~UX, ~"design artifact" and ~"Deliverable" labels, add a milestone -and use a title that makes it clear that the scheduled issue is design only -(e.g. `Design exploration for XYZ`). +The initial issue should be about the problem we are solving. If a separate [product discovery issue](#product-discovery-issues) is needed for additional research and design work, it will be created by a PM or UX person. Assign the ~UX, ~"product discovery" and ~"Deliverable" labels, add a milestone and use a title that makes it clear that the scheduled issue is product discovery +(e.g. `Product discovery for XYZ`). -When the ~"design artifact" issue has been completed, the UXer removes the ~UX +When the ~"product discovery" issue has been completed, the UXer removes the ~UX label, adds the ~"UX ready" label and closes the issue. This indicates the -design artifact is complete. The UXer will also copy the designs to related +UX work for the issue is complete. The UXer will also copy any designs to related issues for implementation in an upcoming milestone. ## Style guides diff --git a/doc/development/diffs.md b/doc/development/diffs.md index 55fc16e0b33..d06339480b1 100644 --- a/doc/development/diffs.md +++ b/doc/development/diffs.md @@ -1,6 +1,6 @@ -# Working with Merge Request diffs +# Working with diffs -Currently we rely on different sources to present merge request diffs, these include: +Currently we rely on different sources to present diffs, these include: - Rugged gem - Gitaly service @@ -11,6 +11,8 @@ We're constantly moving Rugged calls to Gitaly and the progress can be followed ## Architecture overview +### Merge request diffs + When refreshing a Merge Request (pushing to a source branch, force-pushing to target branch, or if the target branch now contains any commits from the MR) we fetch the comparison information using `Gitlab::Git::Compare`, which fetches `base` and `head` data using Gitaly and diff between them through `Gitlab::Git::Diff.between` (which uses _Gitaly_ if it's enabled, otherwise _Rugged_). @@ -32,6 +34,17 @@ In order to present diffs information on the Merge Request diffs page, we: 3. If the diff file is cacheable (text-based), it's cached on Redis using `Gitlab::Diff::FileCollection::MergeRequestDiff` +### Note diffs + +When commenting on a diff (any comparison), we persist a truncated diff version +on `NoteDiffFile` (which is associated with the actual `DiffNote`). So instead +of hitting the repository every time we need the diff of the file, we: + +1. Check whether we have the `NoteDiffFile#diff` persisted and use it +2. Otherwise, if it's a current MR revision, use the persisted +`MergeRequestDiffFile#diff` +3. In the last scenario, go the the repository and fetch the diff + ## Diff limits As explained above, we limit single diff files and the size of the whole diff. There are scenarios where we collapse the diff file, diff --git a/doc/development/documentation/index.md b/doc/development/documentation/index.md index f5cdd310f6f..f46c171d9f1 100644 --- a/doc/development/documentation/index.md +++ b/doc/development/documentation/index.md @@ -25,52 +25,23 @@ them to review it for you. We use the [monthly release blog post](https://about.gitlab.com/handbook/marketing/blog/release-posts/#monthly-releases) as a changelog checklist to ensure everything is documented. -Whenever you submit a merge request for the documentation, use the documentation MR description template. +Whenever you submit a merge request for the documentation, use the +"Documentation" MR description template. If you're changing documentation +location, use the MR description template called "Change documentation +location" instead. -Please check the [documentation workflow](https://about.gitlab.com/handbook/product/technical-writing/workflow/) before getting started. +## Documentation workflow -## Documentation structure - -- Overview and use cases: what it is, why it is necessary, why one would use it -- Requirements: what do we need to get started -- Tutorial: how to set it up, how to use it - -Always link a new document from its topic-related index, otherwise, it will -not be included it in the documentation site search. - -_Note: to be extended._ - -### Feature overview and use cases - -Every major feature (regardless if present in GitLab Community or Enterprise editions) -should present, at the beginning of the document, two main sections: **overview** and -**use cases**. Every GitLab EE-only feature should also contain these sections. +Please read through the [documentation workflow](workflow.md) before getting started. -**Overview**: as the name suggests, the goal here is to provide an overview of the feature. -Describe what is it, what it does, why it is important/cool/nice-to-have, -what problem it solves, and what you can do with this feature that you couldn't -do before. - -**Use cases**: provide at least two, ideally three, use cases for every major feature. -You should answer this question: what can you do with this feature/change? Use cases -are examples of how this feature or change can be used in real life. - -Examples: -- CE and EE: [Issues](../user/project/issues/index.md#use-cases) -- CE and EE: [Merge Requests](../user/project/merge_requests/index.md#overview) -- EE-only: [Geo](https://docs.gitlab.com/ee/gitlab-geo/README.html#overview) -- EE-only: [Jenkins integration](https://docs.gitlab.com/ee/integration/jenkins.md#overview) - -Note that if you don't have anything to add between the doc title (`<h1>`) and -the header `## Overview`, you can omit the header, but keep the content of the -overview there. +## Documentation structure -> **Overview** and **use cases** are required to **every** Enterprise Edition feature, -and for every **major** feature present in Community Edition. +Follow through the [documentation structure guide](structure.md) for learning +how to structure GitLab docs. ## Markdown and styles -Currently GitLab docs use Redcarpet as [markdown](../user/markdown.md) engine, but there's an [open discussion](https://gitlab.com/gitlab-com/gitlab-docs/issues/50) for implementing Kramdown in the near future. +Currently GitLab docs use Redcarpet as [markdown](../../user/markdown.md) engine, but there's an [open discussion](https://gitlab.com/gitlab-com/gitlab-docs/issues/50) for implementing Kramdown in the near future. All the docs follow the [documentation style guidelines](styleguide.md). @@ -84,9 +55,18 @@ In order to have a [solid site structure](https://searchengineland.com/seo-benef all docs should be linked. Every new document should be cross-linked to its related documentation, and linked from its topic-related index, when existent. The directories `/workflow/`, `/gitlab-basics/`, `/university/`, and `/articles/` have -been deprecated and the majority their docs have been moved to their correct location +been **deprecated** and the majority their docs have been moved to their correct location in small iterations. Please don't create new docs in these folders. +### Documentation files + +- When you create a new directory, always start with an `index.md` file. +Do not use another file name and **do not** create `README.md` files +- **Do not** use special chars and spaces, or capital letters in file names, +directory names, branch names, and anything that generates a path. +- Max screenshot size: 100KB +- We do not support videos (yet) + ### Location and naming documents The documentation hierarchy can be vastly improved by providing a better layout @@ -116,7 +96,7 @@ The table below shows what kind of documentation goes where. --- -**General rules:** +**General rules & best practices:** 1. The correct naming and location of a new document, is a combination of the relative URL of the document in question and the GitLab Map design @@ -203,7 +183,7 @@ Things to note: documentation, sometimes it might be useful to search a path deeper. - The `*.md` extension is not used when a document is linked to GitLab's built-in help page, that's why we omit it in `git grep`. -- Use the checklist on the documentation MR description template. +- Use the checklist on the "Change documentation location" MR description template. #### Alternative redirection method @@ -514,7 +494,7 @@ Suppose there's a process to go from point A to point B in 5 steps: `(A) 1 > 2 > A **guide** can be understood as a description of certain processes to achieve a particular objective. A guide brings you from A to B describing the characteristics of that process, but not necessarily going over each step. It can mention, for example, steps 2 and 3, but does not necessarily explain how to accomplish them. -- Live example: "[Static sites and GitLab Pages domains (Part 1)](../user/project/pages/getting_started_part_one.md) to [Creating and Tweaking GitLab CI/CD for GitLab Pages (Part 4)](../../user/project/pages/getting_started_part_four.md)" +- Live example: "[Static sites and GitLab Pages domains (Part 1)](../../user/project/pages/getting_started_part_one.md) to [Creating and Tweaking GitLab CI/CD for GitLab Pages (Part 4)](../../user/project/pages/getting_started_part_four.md)" A **tutorial** requires a clear **step-by-step** guidance to achieve a singular objective. It brings you from A to B, describing precisely all the necessary steps involved in that process, showing each of the 5 steps to go from A to B. It does not only describes steps 2 and 3, but also shows you how to accomplish them. diff --git a/doc/development/documentation/structure.md b/doc/development/documentation/structure.md new file mode 100644 index 00000000000..1002836096a --- /dev/null +++ b/doc/development/documentation/structure.md @@ -0,0 +1,149 @@ +--- +description: Learn the how to correctly structure GitLab documentation. +--- + +# Documentation structure + +For consistency throughout the documentation, it's important to maintain the same +structure among the docs. + +Before getting started, read through the following docs: + +- [Contributing to GitLab documentation](index.md#contributing-to-docs) +- [Merge requests for GitLab documentation](index.md#merge-requests-for-gitlab-documentation) +- [Branch naming for docs-only changes](index.md#branch-naming) +- [Documentation directory structure](index.md#documentation-directory-structure) +- [Documentation style guidelines](styleguide.md) +- [Documentation workflow](workflow.md) + +## Documentation blurb + +Every document should include the following content in the following sequence: + +- **Feature name**: defines an intuitive name for the feature that clearly +states what it is and is consistent with any relevant UI text. +- **Feature overview** and description: describe what it is, what it does, and in what context it should be used. +- **Use cases**: describes real use case scenarios for that feature. +- **Requirements**: describes what software and/or configuration is required to be able to +use the feature and, if applicable, prerequisite knowledge for being able to follow/implement the tutorial. +For example, familiarity with GitLab CI/CD, an account on a third-party service, dependencies installed, etc. +Link each one to its most relevant resource; i.e., where the reader can go to begin to fullfil that requirement. +(Another doc page, a third party application's site, etc.) +- **Instructions**: clearly describes the steps to use the feature, leaving no gaps. +- **Troubleshooting** guide (recommended but not required): if you know beforehand what issues +one might have when setting it up, or when something is changed, or on upgrading, it's +important to describe those too. Think of things that may go wrong and include them in the +docs. This is important to minimize requests for support, and to avoid doc comments with +questions that you know someone might ask. Answering them beforehand only makes your +document better and more approachable. + +For additional details, see the subsections below, as well as the [Documentation template for new docs](#Documentation-template-for-new-docs). + +### Feature overview and use cases + +Every major feature (regardless if present in GitLab Community or Enterprise editions) +should present, at the beginning of the document, two main sections: **overview** and +**use cases**. Every GitLab EE-only feature should also contain these sections. + +**Overview**: as the name suggests, the goal here is to provide an overview of the feature. +Describe what is it, what it does, why it is important/cool/nice-to-have, +what problem it solves, and what you can do with this feature that you couldn't +do before. + +**Use cases**: provide at least two, ideally three, use cases for every major feature. +You should answer this question: what can you do with this feature/change? Use cases +are examples of how this feature or change can be used in real life. + +Examples: +- CE and EE: [Issues](../user/project/issues/index.md#use-cases) +- CE and EE: [Merge Requests](../user/project/merge_requests/index.md#overview) +- EE-only: [Geo](https://docs.gitlab.com/ee/gitlab-geo/README.html#overview) +- EE-only: [Jenkins integration](https://docs.gitlab.com/ee/integration/jenkins.md#overview) + +Note that if you don't have anything to add between the doc title (`<h1>`) and +the header `## Overview`, you can omit the header, but keep the content of the +overview there. + +> **Overview** and **use cases** are required to **every** Enterprise Edition feature, +and for every **major** feature present in Community Edition. + +### Discoverability + +Your new document will be discoverable by the user only if: + +- Crosslinked from the higher-level index (e.g., Issue Boards docs +should be linked from Issues; Prometheus docs should be linked from +Monitoring; CI/CD tutorials should be linked from CI/CD examples). + - 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. +- The headings are clear. E.g., "App testing" is a bad heading, "Testing +an application with GitLab CI/CD" is much better. Think of something +someone will search for and use these keywords in the headings. + +## Documentation template for new docs + +To start a new document, respect the file tree and file name guidelines, +as well as the style guidelines. Use the following template: + +```md +--- +description: "short document description." # Up to ~200 chars long. They will be displayed in Google Search Snippets. +--- + +# Feature Name **[TIER]** (1) + +> [Introduced](link_to_issue_or_mr) in GitLab Tier X.Y (2). + +A short description for the feature (can be the same used in the frontmatter's +`description`). + +## Overview + +To write the feature overview, you should consider answering the following questions: + +- What is it? +- Who is it for? +- What is the context in which it is used and are there any prerequisites/requirements? +- What can the user do with it? (Be sure to consider multiple audiences, like GitLab admin and developer-user.) +- What are the benefits to using it over any alternatives? + +## Use cases + +Describe one to three use cases for that feature. Give real-life examples. + +## Requirements + +State any requirements, if any, for using the feature and/or following along with the tutorial. + +The only assumption that is redundant and doesn't need to be mentioned is having an account +on GitLab. + +## Instructions + +("Instructions" is not necessarily the name of the heading) + +- Write a step-by-step guide, with no gaps between the steps. +- Start with an h2 (`##`), break complex steps into small steps using +subheadings h3 > h4 > h5 > h6. _Never skip the hierarchy level, such +as h2 > h4_, as it will break the TOC and may affect the breadcrumbs. +- Use short and descriptive headings (up to ~50 chars). You can use one +single heading `## How it works` for the instructions when the feature +is simple and the document is short. +- Be clear, concise, and stick to the goal of the doc: explain how to +use that feature. +- Use inclusive language and avoid jargons, as well as uncommon and +fancy words. The docs should be clear and very easy to understand. +- Write in the 3rd person (use "we", "you", "us", "one", instead of "I" or "me"). +- Always provide internal and external reference links. +- Always link the doc from its higher-level index. + +<!-- ## Troubleshooting + +Add a troubleshooting guide when possible/applicable. --> +``` + +Notes: + +- (1): Apply the [tier badges](styleguide.md#product-badges) accordingly +- (2): Apply the correct format for the [GitLab version introducing the feature](styleguide.md#gitlab-versions-and-tiers) diff --git a/doc/development/documentation/styleguide.md b/doc/development/documentation/styleguide.md index ad49c77aac8..6c60a517b6d 100644 --- a/doc/development/documentation/styleguide.md +++ b/doc/development/documentation/styleguide.md @@ -10,6 +10,22 @@ GitLab documentation. Check the Check the GitLab handbook for the [writing styles guidelines](https://about.gitlab.com/handbook/communication/#writing-style-guidelines). +## Files + +- [Directory structure](index.md#location-and-naming-documents): place the docs +in the correct location +- [Documentation files](index.md#documentation-files): name the files accordingly +- [Markdown](../../user/markdown.md): use the GitLab Flavored Markdown in the +documentation + +NOTE: **Note:** +**Do not** use capital letters, spaces, or special chars in file names, +branch names, directory names, headings, or in anything that generates a path. + +NOTE: **Note:** +**Do not** create new `README.md` files, name them `index.md` instead. There's +a test that will fail if it spots a new `README.md` file. + ## Text - Split up long lines (wrap text), this makes it much easier to review and edit. Only @@ -61,7 +77,8 @@ For punctuation rules, please refer to the [GitLab UX guide](https://design.gitl - Add **only one H1** in each document, by adding `#` at the beginning of it (when using markdown). The `h1` will be the document `<title>`. -- For subheadings, use `##`, `###` and so on +- Start with an h2 (`##`), and respect the order h2 > h3 > h4 > h5 > h6. + Never skip the hierarchy level, such as h2 > h4 - Avoid putting numbers in headings. Numbers shift, hence documentation anchor links shift too, which eventually leads to dead links. If you think it is compelling to add numbers in headings, make sure to at least discuss it with @@ -115,10 +132,7 @@ needs to expand the tab to find the settings you're referring to the `.md` document that you're working on is located. Always prepend their names with the name of the document that they will be included in. For example, if there is a document called `twitter.md`, then a valid image name - could be `twitter_login_screen.png`. [**Exception**: images for - [articles](index.md#technical-articles) should be - put in a directory called `img` underneath `/articles/article_title/img/`, therefore, - there's no need to prepend the document name to their filenames.] + could be `twitter_login_screen.png`. - Images should have a specific, non-generic name that will differentiate them. - Keep all file names in lower case. - Consider using PNG images instead of JPEG. @@ -126,6 +140,8 @@ needs to expand the tab to find the settings you're referring to - 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. +- Max image size: 100KB (gifs included). +- The GitLab docs do not support videos yet. Inside the document: diff --git a/doc/development/documentation/workflow.md b/doc/development/documentation/workflow.md new file mode 100644 index 00000000000..339ec80f889 --- /dev/null +++ b/doc/development/documentation/workflow.md @@ -0,0 +1,186 @@ +--- +description: Learn the process of shipping documentation for GitLab. +--- + +# Documentation process at GitLab + +At GitLab, developers contribute new or updated documentation along with their code, but product managers and technical writers also have essential roles in the process. + +- Product Managers (PMs): in the issue for all new and updated features, +PMs include specific documentation requirements that the developer who is +writing or updating the docs must meet, along with feature descriptions +and use cases. They call out any specific areas where collaborating with +a technical writer is recommended, and usually act as the first reviewer +of the docs. +- Developers: author documentation and merge it on time (up to a week after +the feature freeze). +- Technical Writers: review each issue to ensure PM's requirements are complete, +help developers with any questions throughout the process, and act as the final +reviewer of all new and updated docs content before it's merged. + +## Requirements + +Documentation must be delivered whenever: + +- A new feature is shipped +- There are changes to the UI +- A process, workflow, or previously documented feature is changed + +Documentation is not required when a feature is changed on the backend +only and does not directly affect the way that any regular user or +administrator would interact with GitLab. + +NOTE: **Note:** +When refactoring documentation in needed, it should be submitted it in its own MR. +**Do not** join new features' MRs with refactoring existing docs, as they might have +different priorities. + +NOTE: **Note:** +[Smaller MRs are better](https://gitlab.com/gitlab-com/blog-posts/issues/185#note_4401010)! Do not mix subjects, and ship the smallest MR possible. + +### Documentation review process + +The docs shipped by the developer should be reviewed by the PM (for accuracy) and a Technical Writer (for clarity and structure). + +#### Documentation updates that require Technical Writer review + +Every documentation change that meets the criteria below must be reviewed by a Technical Writer +to ensure clarity and discoverability, and avoid redundancy, bad file locations, typos, broken links, etc. +Within the GitLab issue or MR, ping the relevant technical writer for the subject area. If you're not sure who that is, +ping any of them or all of them (`@gl\-docsteam`). + +A Technical Writer must review documentation updates that involve: + +- Docs introducing new features +- Changing documentation location +- Refactoring existing documentation +- Creating new documentation files + +If you need any help to choose the correct place for a doc, discuss a documentation +idea or outline, or request any other help, ping a Technical Writer on your issue, MR, +or on Slack in `#docs`. + +#### Skip the PM's review + +When there's a non-significant change to the docs, you can skip the review +of the PM. Add the same labels as you would for a regular doc change and +assign the correct milestone. In these cases, assign a Technical Writer +for approval/merge, or mention `@gl\-docsteam` in case you don't know +which Tech Writer to assign for. + +#### Skip the entire review + +When the MR only contains corrections to the content (typos, grammar, +broken links, etc), it can be merged without the PM's and Tech Writer's review. + +## Documentation structure + +Read through the [documentation structure](structure.md) docs for an overview. + +## Documentation workflow + +To follow a consistent workflow every month, documentation changes +involve the Product Managers, the developer who shipped the feature, +and the Technical Writing team. Each role is described below. + +### 1. Product Manager's role in the documentation process + +The Product Manager (PM) should add to the feature issue: + +- Feature name, overview/description, and use cases, for the [documentation blurb](structure.md#documentation-blurb) +- The documentation requirements for the developer working on the docs + - What new page, new subsection of an existing page, or other update to an existing page/subsection is needed. + - Just one page/section/update or multiple (perhaps there's an end user and admin change needing docs, or we need to update a previously recommended workflow, or we want to link the new feature from various places; consider and mention all ways documentation should be affected + - Suggested title of any page or subsection, if applicable +- Label the issue with `Documentation`, `Deliverable`, `docs:P1`, and assign + the correct milestone + +### 2. Developer's role in the documentation process + +As a developer, or as a community contributor, you should ship the documentation +with the feature, as in GitLab the documentation is part of the product. + +The docs can either be shipped along with the MR introducing the code, or, +alternatively, created from a follow-up issue and MR. + +The docs should be shipped **by the feature freeze date**. Justified +exceptions are accepted, as long as the [following process](#documentation-shipped-late) +and the missed-deliverable due date (the 14th of each month) are both respected. + +#### Documentation shipped in the feature MR + +The developer should add to the feature MR the documentation containing: + +- The [documentation blurb](structure.md#documentation-blurb): copy the +feature name, overview/description, and use cases from the feature issue +- Instructions: write how to use the feature, step by step, with no gaps. +- [Crosslink for discoverability](structure.md#discoverability): link with +internal docs and external resources (if applicable) +- Index: link the new doc or the new heading from the higher-level index +for [discoverability](#discoverability) +- [Screenshots](styleguide.md#images): when necessary, add screenshots for: + - Illustrating a step of the process + - Indicating the location of a navigation menu +- Label the MR with `Documentation`, `Deliverable`, `docs-P1`, and assign +the correct milestone +- Assign the PM for review +- When done, mention the `@gl\-docsteam` in the MR asking for review +- **Due date**: feature freeze date and time + +#### Documentation shipped in a follow-up MR + +If the docs aren't being shipped within the feature MR: + +- Create a new issue mentioning "docs" or "documentation" in the title (use the Documentation issue description template) +- Label the issue with: `Documentation`, `Deliverable`, `docs-P1`, `<product-label>` +(product label == CI/CD, Pages, Prometheus, etc) +- Add the correct milestone +- Create a new MR for shipping the docs changes and follow the same +process [described above](#documentation-shipped-in-the-feature-mr) +- Use the MR description template called "Documentation" +- Add the same labels and milestone as you did for the issue +- Assign the PM for review +- When done, mention the `@gl\-docsteam` in the MR asking for review +- **Due date**: feature freeze date and time + +#### Documentation shipped late + +Shipping late means that you are affecting the whole feature workflow +as well as other teams' priorities (PMs, tech writers, release managers, +release post reviewers), so every effort should be made to avoid this. + +If you did not ship the docs within the feature freeze, proceed as +[described above](#documentation-shipped-in-a-follow-up-mr) and, +besides the regular labels, include the labels `Pick into X.Y` and +`missed-deliverable` in the issue and the MR, and assign them the correct +milestone. + +The **due date** for **merging** `missed-deliverable` MRs is on the +**14th** of each month. + +### 3. Technical Writer's role in the documentation process + +- **Planning** + - Once an issue contains a Documentation label and the current milestone, a +technical writer reviews the Product Manager's documentation requirements + - Once the documentation requirements are approved, the technical writer can +work with the developer to discuss any documentation questions and plans/outlines, as needed. + +- **Review** - A technical writer must review the documentation for: + - Clarity + - Relevance (make sure the content is appropriate given the impact of the feature) + - Location (make sure the doc is in the correct dir and has the correct name) + - Syntax, typos, and broken links + - Improvements to the content + - Accordance to the [docs style guide](styleguide.md) + +<!-- TBA: issue and MR description templates as part of the process --> + +<!-- +## New features vs feature updates + +- TBA: + - Describe the difference between new features and feature updates + - Creating a new doc vs updating an existing doc +--> + diff --git a/doc/development/ee_features.md b/doc/development/ee_features.md index 32de741c9fe..1cd873b6fe3 100644 --- a/doc/development/ee_features.md +++ b/doc/development/ee_features.md @@ -258,6 +258,31 @@ end [`extend ::Gitlab::Utils::Override`]: utilities.md#override +##### Overriding CE class methods + +The same applies to class methods, except we want to use +`ActiveSupport::Concern` and put `extend ::Gitlab::Utils::Override` +within the block of `class_methods`. Here's an example: + +```ruby +module EE + module Groups + module GroupMembersController + extend ActiveSupport::Concern + + class_methods do + extend ::Gitlab::Utils::Override + + override :admin_not_required_endpoints + def admin_not_required_endpoints + super.concat(%i[update override]) + end + end + end + end +end +``` + #### Use self-descriptive wrapper methods When it's not possible/logical to modify the implementation of a @@ -665,6 +690,9 @@ module EE extend ActiveSupport::Concern class_methods do + extend ::Gitlab::Utils::Override + + override :update_params_at_least_one_of def update_params_at_least_one_of super.push(*%i[ squash diff --git a/doc/development/feature_flags.md b/doc/development/feature_flags.md index 5d1f657015c..09ea8c05be6 100644 --- a/doc/development/feature_flags.md +++ b/doc/development/feature_flags.md @@ -20,7 +20,40 @@ dynamic (querying the DB etc.). Once defined in `lib/feature.rb`, you will be able to activate a feature for a given feature group via the [`feature_group` param of the features API](../api/features.md#set-or-create-a-feature) +For GitLab.com, team members have access to feature flags through chatops. Only +percentage gates are supported at this time. Setting a feature to be used 50% of +the time, you should execute `/chatops run feature set my_feature_flag 50`. + ## Feature flags for user applications GitLab does not yet support the use of feature flags in deployed user applications. -You can follow the progress on that [in the issue on our issue tracker](https://gitlab.com/gitlab-org/gitlab-ee/issues/779).
\ No newline at end of file +You can follow the progress on that [in the issue on our issue tracker](https://gitlab.com/gitlab-org/gitlab-ee/issues/779). + +## Developing with feature flags + +In general, it's better to have a group- or user-based gate, and you should prefer +it over the use of percentage gates. This would make debugging easier, as you +filter for example logs and errors based on actors too. Futhermore, this allows +for enabling for the `gitlab-org` group first, while the rest of the users +aren't impacted. + +```ruby +# Good +Feature.enabled?(:feature_flag, project) + +# Avoid, if possible +Feature.enabled?(:feature_flag) +``` + +To use feature gates based on actors, the model needs to respond to +`flipper_id`. For example, to enable for the Foo model: + +```ruby +class Foo < ActiveRecord::Base + include FeatureGate +end +``` + +Features that are developed and are intended to be merged behind a feature flag +should not include a changelog entry. The entry should be added in the merge +request removing the feature flags. diff --git a/doc/development/migration_style_guide.md b/doc/development/migration_style_guide.md index a211effdfa7..6f31e5b82e5 100644 --- a/doc/development/migration_style_guide.md +++ b/doc/development/migration_style_guide.md @@ -182,6 +182,34 @@ class MyMigration < ActiveRecord::Migration end ``` +## Adding foreign-key constraints + +When adding a foreign-key constraint to either an existing or new +column remember to also add a index on the column. + +This is _required_ if the foreign-key constraint specifies +`ON DELETE CASCADE` or `ON DELETE SET NULL` behavior. On a cascading +delete, the [corresponding record needs to be retrieved using an +index](https://www.cybertec-postgresql.com/en/postgresql-indexes-and-foreign-keys/) +(otherwise, we'd need to scan the whole table) for subsequent update or +deletion. + +Here's an example where we add a new column with a foreign key +constraint. Note it includes `index: true` to create an index for it. + +```ruby +class Migration < ActiveRecord::Migration + + def change + add_reference :model, :other_model, index: true, foreign_key: { on_delete: :cascade } + end +end +``` + +When adding a foreign-key constraint to an existing column, we +have to employ `add_concurrent_foreign_key` and `add_concurrent_index` +instead of `add_reference`. + ## Adding Columns With Default Values When adding columns with default values you must use the method diff --git a/doc/development/new_fe_guide/development/testing.md b/doc/development/new_fe_guide/development/testing.md index e1e13474b75..53dfe6774e9 100644 --- a/doc/development/new_fe_guide/development/testing.md +++ b/doc/development/new_fe_guide/development/testing.md @@ -133,3 +133,27 @@ afterEach(() => { vm.$destroy(); }); ``` +## Testing with older browsers + +Some regressions only affect a specific browser version. We can install and test in particular browsers with either Firefox or Browserstack using the following steps: + + +### Browserstack + +[Browserstack](https://www.browserstack.com/) allows you to test more than 1200 mobile devices and browsers. +You can use it directly through the [live app](https://www.browserstack.com/live) or you can install the [chrome extension](https://chrome.google.com/webstore/detail/browserstack/nkihdmlheodkdfojglpcjjmioefjahjb) for easy access. +You can find the credentials on 1Password, under `frontendteam@gitlab.com`. + +### Firefox + +#### macOS +You can download any older version of Firefox from the releases FTP server, https://ftp.mozilla.org/pub/firefox/releases/ + +1. From the website, select a version, in this case `50.0.1`. +2. Go to the mac folder. +3. Select your preferred language, you will find the dmg package inside, download it. +4. Drag and drop the application to any other folder but the `Applications` folder. +5. Rename the application to something like `Firefox_Old`. +6. Move the application to the `Applications` folder. +7. Open up a terminal and run `/Applications/Firefox_Old.app/Contents/MacOS/firefox-bin -profilemanager` to create a new profile specific to that Firefox version. +8. Once the profile has been created, quit the app, and run it again like normal. You now have a working older Firefox version. diff --git a/doc/development/testing_guide/smoke.md b/doc/development/testing_guide/smoke.md new file mode 100644 index 00000000000..3f2843cba6e --- /dev/null +++ b/doc/development/testing_guide/smoke.md @@ -0,0 +1,16 @@ +# Smoke Tests + +It is imperative in any testing suite that we have Smoke Tests. In short, smoke tests are will run quick sanity +end-to-end functional tests from GitLab QA and are designed to run against the specified environment to ensure that +basic functionality is working. + +Currently, our suite consists of this basic functionality coverage: + +- User Login (Standard Auth) +- Project Creation +- Issue Creation +- Merge Request Creation + +--- + +[Return to Testing documentation](index.md) diff --git a/doc/development/testing_guide/testing_levels.md b/doc/development/testing_guide/testing_levels.md index 07ced36f0c1..4403072e96f 100644 --- a/doc/development/testing_guide/testing_levels.md +++ b/doc/development/testing_guide/testing_levels.md @@ -120,6 +120,14 @@ running feature tests (i.e. using Capybara) against it. The actual test scenarios and steps are [part of GitLab Rails] so that they're always in-sync with the codebase. +### Smoke tests + +Smoke tests are quick tests that may be run at any time (especially after the pre-deployment migrations). + +Much like feature tests - these tests run against the UI and ensure that basic functionality is working. + +> See [Smoke Tests](smoke.md) for more information. + Read a separate document about [end-to-end tests](end_to_end_tests.md) to learn more. diff --git a/doc/development/understanding_explain_plans.md b/doc/development/understanding_explain_plans.md new file mode 100644 index 00000000000..adf8795a5e3 --- /dev/null +++ b/doc/development/understanding_explain_plans.md @@ -0,0 +1,676 @@ +# Understanding EXPLAIN plans + +PostgreSQL allows you to obtain query plans using the `EXPLAIN` command. This +command can be invaluable when trying to determine how a query will perform. +You can use this command directly in your SQL query, as long as the query starts +with it: + +```sql +EXPLAIN +SELECT COUNT(*) +FROM projects +WHERE visibility_level IN (0, 20); +``` + +When running this on GitLab.com, we are presented with the following output: + +``` +Aggregate (cost=922411.76..922411.77 rows=1 width=8) + -> Seq Scan on projects (cost=0.00..908044.47 rows=5746914 width=0) + Filter: (visibility_level = ANY ('{0,20}'::integer[])) +``` + +When using _just_ `EXPLAIN`, PostgreSQL won't actually execute our query, +instead it produces an _estimated_ execution plan based on the available +statistics. This means the actual plan can differ quite a bit. Fortunately, +PostgreSQL provides us with the option to execute the query as well. To do so, +we need to use `EXPLAIN ANALYZE` instead of just `EXPLAIN`: + +```sql +EXPLAIN ANALYZE +SELECT COUNT(*) +FROM projects +WHERE visibility_level IN (0, 20); +``` + +This will produce: + +``` +Aggregate (cost=922420.60..922420.61 rows=1 width=8) (actual time=3428.535..3428.535 rows=1 loops=1) + -> Seq Scan on projects (cost=0.00..908053.18 rows=5746969 width=0) (actual time=0.041..2987.606 rows=5746940 loops=1) + Filter: (visibility_level = ANY ('{0,20}'::integer[])) + Rows Removed by Filter: 65677 +Planning time: 2.861 ms +Execution time: 3428.596 ms +``` + +As we can see this plan is quite different, and includes a lot more data. Let's +discuss this step by step. + +Because `EXPLAIN ANALYZE` executes the query, care should be taken when using a +query that will write data or might time out. If the query modifies data, +consider wrapping it in a transaction that rolls back automatically like so: + +```sql +BEGIN; +EXPLAIN ANALYZE +DELETE FROM users WHERE id = 1; +ROLLBACK; +``` + +The `EXPLAIN` command also takes additional options, such as `BUFFERS`: + +```sql +EXPLAIN (ANALYZE, BUFFERS) +SELECT COUNT(*) +FROM projects +WHERE visibility_level IN (0, 20); +``` + +This will then produce: + +``` +Aggregate (cost=922420.60..922420.61 rows=1 width=8) (actual time=3428.535..3428.535 rows=1 loops=1) + Buffers: shared hit=208846 + -> Seq Scan on projects (cost=0.00..908053.18 rows=5746969 width=0) (actual time=0.041..2987.606 rows=5746940 loops=1) + Filter: (visibility_level = ANY ('{0,20}'::integer[])) + Rows Removed by Filter: 65677 + Buffers: shared hit=208846 +Planning time: 2.861 ms +Execution time: 3428.596 ms +``` + +For more information, refer to the official [EXPLAIN +documentation](https://www.postgresql.org/docs/current/static/sql-explain.html). + +## Nodes + +Every query plan consists of nodes. Nodes can be nested, and are executed from +the inside out. This means that the innermost node is executed before an outer +node. This can be best thought of as nested function calls, returning their +results as they unwind. For example, a plan starting with an `Aggregate` +followed by a `Nested Loop`, followed by an `Index Only scan` can be thought of +as the following Ruby code: + +```ruby +aggregate( + nested_loop( + index_only_scan() + index_only_scan() + ) +) +``` + +Nodes are indicated using a `->` followed by the type of node taken. For +example: + +``` +Aggregate (cost=922411.76..922411.77 rows=1 width=8) + -> Seq Scan on projects (cost=0.00..908044.47 rows=5746914 width=0) + Filter: (visibility_level = ANY ('{0,20}'::integer[])) +``` + +Here the first node executed is `Seq scan on projects`. The `Filter:` is an +additional filter applied to the results of the node. A filter is very similar +to Ruby's `Array#select`: it takes the input rows, applies the filter, and +produces a new list of rows. Once the node is done, we perform the `Aggregate` +above it. + +Nested nodes will look like this: + +``` +Aggregate (cost=176.97..176.98 rows=1 width=8) (actual time=0.252..0.252 rows=1 loops=1) + Buffers: shared hit=155 + -> Nested Loop (cost=0.86..176.75 rows=87 width=0) (actual time=0.035..0.249 rows=36 loops=1) + Buffers: shared hit=155 + -> Index Only Scan using users_pkey on users users_1 (cost=0.43..4.95 rows=87 width=4) (actual time=0.029..0.123 rows=36 loops=1) + Index Cond: (id < 100) + Heap Fetches: 0 + -> Index Only Scan using users_pkey on users (cost=0.43..1.96 rows=1 width=4) (actual time=0.003..0.003 rows=1 loops=36) + Index Cond: (id = users_1.id) + Heap Fetches: 0 +Planning time: 2.585 ms +Execution time: 0.310 ms +``` + +Here we first perform two separate "Index Only" scans, followed by performing a +"Nested Loop" on the result of these two scans. + +## Node statistics + +Each node in a plan has a set of associated statistics, such as the cost, the +number of rows produced, the number of loops performed, and more. For example: + +``` +Seq Scan on projects (cost=0.00..908044.47 rows=5746914 width=0) +``` + +Here we can see that our cost ranges from `0.00..908044.47` (we'll cover this in +a moment), and we estimate (since we're using `EXPLAIN` and not `EXPLAIN +ANALYZE`) a total of 5,746,914 rows to be produced by this node. The `width` +statistics describes the estimated width of each row, in bytes. + +The `costs` field specifies how expensive a node was. The cost is measured in +arbitrary units determined by the query planner's cost parameters. What +influences the costs depends on a variety of settings, such as `seq_page_cost`, +`cpu_tuple_cost`, and various others. +The format of the costs field is as follows: + +``` +STARTUP COST..TOTAL COST +``` + +The startup cost states how expensive it was to start the node, with the total +cost describing how expensive the entire node was. In general: the greater the +values, the more expensive the node. + +When using `EXPLAIN ANALYZE`, these statistics will also include the actual time +(in milliseconds) spent, and other runtime statistics (e.g. the actual number of +produced rows): + +``` +Seq Scan on projects (cost=0.00..908053.18 rows=5746969 width=0) (actual time=0.041..2987.606 rows=5746940 loops=1) +``` + +Here we can see we estimated 5,746,969 rows to be returned, but in reality we +returned 5,746,940 rows. We can also see that _just_ this sequential scan took +2.98 seconds to run. + +Using `EXPLAIN (ANALYZE, BUFFERS)` will also give us information about the +number of rows removed by a filter, the number of buffers used, and more. For +example: + +``` +Seq Scan on projects (cost=0.00..908053.18 rows=5746969 width=0) (actual time=0.041..2987.606 rows=5746940 loops=1) + Filter: (visibility_level = ANY ('{0,20}'::integer[])) + Rows Removed by Filter: 65677 + Buffers: shared hit=208846 +``` + +Here we can see that our filter has to remove 65,677 rows, and that we use +208,846 buffers. Each buffer in PostgreSQL is 8 KB (8192 bytes), meaning our +above node uses *1.6 GB of buffers*. That's a lot! + +## Node types + +There are quite a few different types of nodes, so we only cover some of the +more common ones here. + +A full list of all the available nodes and their descriptions can be found in +the [PostgreSQL source file +"plannodes.h"](https://github.com/postgres/postgres/blob/master/src/include/nodes/plannodes.h) + +### Seq Scan + +A sequential scan over (a chunk of) a database table. This is like using +`Array#each`, but on a database table. Sequential scans can be quite slow when +retrieving lots of rows, so it's best to avoid these for large tables. + +### Index Only Scan + +A scan on an index that did not require fetching anything from the table. In +certain cases an index only scan may still fetch data from the table, in this +case the node will include a `Heap Fetches:` statistic. + +### Index Scan + +A scan on an index that required retrieving some data from the table. + +### Bitmap Index Scan and Bitmap Heap scan + +Bitmap scans fall between sequential scans and index scans. These are typically +used when we would read too much data from an index scan, but too little to +perform a sequential scan. A bitmap scan uses what is known as a [bitmap +index](https://en.wikipedia.org/wiki/Bitmap_index) to perform its work. + +The [source code of PostgreSQL](https://github.com/postgres/postgres/blob/1c2cb2744bf3d8ad751cd5cf3b347f10f48492b3/src/include/nodes/plannodes.h#L446-L457) +states the following on bitmap scans: + +> Bitmap Index Scan delivers a bitmap of potential tuple locations; it does not +> access the heap itself. The bitmap is used by an ancestor Bitmap Heap Scan +> node, possibly after passing through intermediate Bitmap And and/or Bitmap Or +> nodes to combine it with the results of other Bitmap Index Scans. + +### Limit + +Applies a `LIMIT` on the input rows. + +### Sort + +Sorts the input rows as specified using an `ORDER BY` statement. + +### Nested Loop + +A nested loop will execute its child nodes for every row produced by a node that +precedes it. For example: + +``` +-> Nested Loop (cost=0.86..176.75 rows=87 width=0) (actual time=0.035..0.249 rows=36 loops=1) + Buffers: shared hit=155 + -> Index Only Scan using users_pkey on users users_1 (cost=0.43..4.95 rows=87 width=4) (actual time=0.029..0.123 rows=36 loops=1) + Index Cond: (id < 100) + Heap Fetches: 0 + -> Index Only Scan using users_pkey on users (cost=0.43..1.96 rows=1 width=4) (actual time=0.003..0.003 rows=1 loops=36) + Index Cond: (id = users_1.id) + Heap Fetches: 0 +``` + +Here the first child node (`Index Only Scan using users_pkey on users users_1`) +produces 36 rows, and is executed once (`rows=36 loops=1`). The next node +produces 1 row (`rows=1`), but is repeated 36 times (`loops=36`). This is +because the previous node produced 36 rows. + +This means that nested loops can quickly slow the query down if the various +child nodes keep producing many rows. + +## Optimising queries + +With that out of the way, let's see how we can optimise a query. Let's use the +following query as an example: + +```sql +SELECT COUNT(*) +FROM users +WHERE twitter != ''; +``` + +This query simply counts the number of users that have a Twitter profile set. +Let's run this using `EXPLAIN (ANALYZE, BUFFERS)`: + +```sql +EXPLAIN (ANALYZE, BUFFERS) +SELECT COUNT(*) +FROM users +WHERE twitter != ''; +``` + +This will produce the following plan: + +``` +Aggregate (cost=845110.21..845110.22 rows=1 width=8) (actual time=1271.157..1271.158 rows=1 loops=1) + Buffers: shared hit=202662 + -> Seq Scan on users (cost=0.00..844969.99 rows=56087 width=0) (actual time=0.019..1265.883 rows=51833 loops=1) + Filter: ((twitter)::text <> ''::text) + Rows Removed by Filter: 2487813 + Buffers: shared hit=202662 +Planning time: 0.390 ms +Execution time: 1271.180 ms +``` + +From this query plan we can see the following: + +1. We need to perform a sequential scan on the `users` table. +1. This sequential scan filters out 2,487,813 rows using a `Filter`. +1. We use 202,622 buffers, which equals 1.58 GB of memory. +1. It takes us 1.2 seconds to do all of this. + +Considering we are just counting users, that's quite expensive! + +Before we start making any changes, let's see if there are any existing indexes +on the `users` table that we might be able to use. We can obtain this +information by running `\d users` in a `psql` console, then scrolling down to +the `Indexes:` section: + +``` +Indexes: + "users_pkey" PRIMARY KEY, btree (id) + "users_confirmation_token_key" UNIQUE CONSTRAINT, btree (confirmation_token) + "users_email_key" UNIQUE CONSTRAINT, btree (email) + "users_reset_password_token_key" UNIQUE CONSTRAINT, btree (reset_password_token) + "index_on_users_lower_email" btree (lower(email::text)) + "index_on_users_lower_username" btree (lower(username::text)) + "index_on_users_name_lower" btree (lower(name::text)) + "index_users_on_admin" btree (admin) + "index_users_on_created_at" btree (created_at) + "index_users_on_email_trigram" gin (email gin_trgm_ops) + "index_users_on_feed_token" btree (feed_token) + "index_users_on_ghost" btree (ghost) + "index_users_on_incoming_email_token" btree (incoming_email_token) + "index_users_on_name" btree (name) + "index_users_on_name_trigram" gin (name gin_trgm_ops) + "index_users_on_state" btree (state) + "index_users_on_state_and_internal_attrs" btree (state) WHERE ghost <> true AND support_bot <> true + "index_users_on_support_bot" btree (support_bot) + "index_users_on_username" btree (username) + "index_users_on_username_trigram" gin (username gin_trgm_ops) +``` + +Here we can see there is no index on the `twitter` column, which means +PostgreSQL has to perform a sequential scan in this case. Let's try to fix this +by adding the following index: + +```sql +CREATE INDEX CONCURRENTLY twitter_test ON users (twitter); +``` + +If we now re-run our query using `EXPLAIN (ANALYZE, BUFFERS)` we get the +following plan: + +``` +Aggregate (cost=61002.82..61002.83 rows=1 width=8) (actual time=297.311..297.312 rows=1 loops=1) + Buffers: shared hit=51854 dirtied=19 + -> Index Only Scan using twitter_test on users (cost=0.43..60873.13 rows=51877 width=0) (actual time=279.184..293.532 rows=51833 loops=1) + Filter: ((twitter)::text <> ''::text) + Rows Removed by Filter: 2487830 + Heap Fetches: 26037 + Buffers: shared hit=51854 dirtied=19 +Planning time: 0.191 ms +Execution time: 297.334 ms +``` + +Now it takes just under 300 milliseconds to get our data, instead of 1.2 +seconds. However, we still use 51,854 buffers, which is about 400 MB of memory. +300 milliseconds is also quite slow for such a simple query. To understand why +this query is still expensive, let's take a look at the following: + +``` +Index Only Scan using twitter_test on users (cost=0.43..60873.13 rows=51877 width=0) (actual time=279.184..293.532 rows=51833 loops=1) + Filter: ((twitter)::text <> ''::text) + Rows Removed by Filter: 2487830 +``` + +We start with an index only scan on our index, but we somehow still apply a +`Filter` that filters out 2,487,830 rows. Why is that? Well, let's look at how +we created the index: + +```sql +CREATE INDEX CONCURRENTLY twitter_test ON users (twitter); +``` + +We simply told PostgreSQL to index all possible values of the `twitter` column, +even empty strings. Our query in turn uses `WHERE twitter != ''`. This means +that the index does improve things, as we don't need to do a sequential scan, +but we may still encounter empty strings. This means PostgreSQL _has_ to apply a +Filter on the index results to get rid of those values. + +Fortunately, we can improve this even further using "partial indexes". Partial +indexes are indexes with a `WHERE` condition that is applied when indexing data. +For example: + +```sql +CREATE INDEX CONCURRENTLY some_index ON users (email) WHERE id < 100 +``` + +This index would only index the `email` value of rows that match `WHERE id < +100`. We can use partial indexes to change our Twitter index to the following: + +```sql +CREATE INDEX CONCURRENTLY twitter_test ON users (twitter) WHERE twitter != ''; +``` + +Once created, if we run our query again we will be given the following plan: + +``` +Aggregate (cost=1608.26..1608.27 rows=1 width=8) (actual time=19.821..19.821 rows=1 loops=1) + Buffers: shared hit=44036 + -> Index Only Scan using twitter_test on users (cost=0.41..1479.71 rows=51420 width=0) (actual time=0.023..15.514 rows=51833 loops=1) + Heap Fetches: 1208 + Buffers: shared hit=44036 +Planning time: 0.123 ms +Execution time: 19.848 ms +``` + +That's _a lot_ better! Now it only takes 20 milliseconds to get the data, and we +only use about 344 MB of buffers (instead of the original 1.58 GB). The reason +this works is that now PostgreSQL no longer needs to apply a `Filter`, as the +index only contains `twitter` values that are not empty. + +Keep in mind that you shouldn't just add partial indexes every time you want to +optimise a query. Every index has to be updated for every write, and they may +require quite a bit of space, depending on the amount of indexed data. As a +result, first check if there are any existing indexes you may be able to reuse. +If there aren't any, check if you can perhaps slightly change an existing one to +fit both the existing and new queries. Only add a new index if none of the +existing indexes can be used in any way. + +## Queries that can't be optimised + +Now that we have seen how to optimise a query, let's look at another query that +we might not be able to optimise: + +```sql +EXPLAIN (ANALYZE, BUFFERS) +SELECT COUNT(*) +FROM projects +WHERE visibility_level IN (0, 20); +``` + +The output of `EXPLAIN (ANALYZE, BUFFERS)` is as follows: + +``` +Aggregate (cost=922420.60..922420.61 rows=1 width=8) (actual time=3428.535..3428.535 rows=1 loops=1) + Buffers: shared hit=208846 + -> Seq Scan on projects (cost=0.00..908053.18 rows=5746969 width=0) (actual time=0.041..2987.606 rows=5746940 loops=1) + Filter: (visibility_level = ANY ('{0,20}'::integer[])) + Rows Removed by Filter: 65677 + Buffers: shared hit=208846 +Planning time: 2.861 ms +Execution time: 3428.596 ms +``` + +Looking at the output we see the following Filter: + +``` +Filter: (visibility_level = ANY ('{0,20}'::integer[])) +Rows Removed by Filter: 65677 +``` + +Looking at the number of rows removed by the filter, we may be tempted to add an +index on `projects.visibility_level` to somehow turn this Sequential scan + +filter into an index-only scan. + +Unfortunately, doing so is unlikely to improve anything. Contrary to what some +might believe, an index being present _does not guarantee_ that PostgreSQL will +actually use it. For example, when doing a `SELECT * FROM projects` it is much +cheaper to just scan the entire table, instead of using an index and then +fetching data from the table. In such cases PostgreSQL may decide to not use an +index. + +Second, let's think for a moment what our query does: it gets all projects with +visibility level 0 or 20. In the above plan we can see this produces quite a lot +of rows (5,745,940), but how much is that relative to the total? Let's find out +by running the following query: + +```sql +SELECT visibility_level, count(*) AS amount +FROM projects +GROUP BY visibility_level +ORDER BY visibility_level ASC; +``` + +For GitLab.com this produces: + +``` + visibility_level | amount +------------------+--------- + 0 | 5071325 + 10 | 65678 + 20 | 674801 +``` + +Here the total number of projects is 5,811,804, and 5,746,126 of those are of +level 0 or 20. That's 98% of the entire table! + +So no matter what we do, this query will retrieve 98% of the entire table. Since +most time is spent doing exactly that, there isn't really much we can do to +improve this query, other than _not_ running it at all. + +What is important here is that while some may recommend to straight up add an +index the moment you see a sequential scan, it is _much more important_ to first +understand what your query does, how much data it retrieves, and so on. After +all, you can not optimise something you do not understand. + +### Cardinality and selectivity + +Earlier we saw that our query had to retrieve 98% of the rows in the table. +There are two terms commonly used for databases: cardinality, and selectivity. +Cardinality refers to the number of unique values in a particular column in a +table. + +Selectivity is the number of unique values produced by an operation (e.g. an +index scan or filter), relative to the total number of rows. The higher the +selectivity, the more likely PostgreSQL is able to use an index. + +In the above example, there are only 3 unique values: 0, 10, and 20. This means +the cardinality is 3. The selectivity in turn is also very low: 0.0000003% (2 / +5,811,804), because our `Filter` only filters using two values (`0` and `20`). +With such a low selectivity value it's not surprising that PostgreSQL decides +using an index is not worth it, because it would produce almost no unique rows. + +## Rewriting queries + +So the above query can't really be optimised as-is, or at least not much. But +what if we slightly change the purpose of it? What if instead of retrieving all +projects with `visibility_level` 0 or 20, we retrieve those that a user +interacted with somehow? + +Fortunately, GitLab has an answer for this, and it's a table called +`user_interacted_projects`. This table has the following schema: + +``` +Table "public.user_interacted_projects" + Column | Type | Modifiers +------------+---------+----------- + user_id | integer | not null + project_id | integer | not null +Indexes: + "index_user_interacted_projects_on_project_id_and_user_id" UNIQUE, btree (project_id, user_id) + "index_user_interacted_projects_on_user_id" btree (user_id) +Foreign-key constraints: + "fk_rails_0894651f08" FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE + "fk_rails_722ceba4f7" FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE +``` + +Let's rewrite our query to JOIN this table onto our projects, and get the +projects for a specific user: + +```sql +EXPLAIN ANALYZE +SELECT COUNT(*) +FROM projects +INNER JOIN user_interacted_projects ON user_interacted_projects.project_id = projects.id +WHERE projects.visibility_level IN (0, 20) +AND user_interacted_projects.user_id = 1; +``` + +What we do here is the following: + +1. Get our projects. +1. INNER JOIN `user_interacted_projects`, meaning we're only left with rows in + `projects` that have a corresponding row in `user_interacted_projects`. +1. Limit this to the projects with `visibility_level` of 0 or 20, and to + projects that the user with ID 1 interacted with. + +If we run this query we get the following plan: + +``` + Aggregate (cost=871.03..871.04 rows=1 width=8) (actual time=9.763..9.763 rows=1 loops=1) + -> Nested Loop (cost=0.86..870.52 rows=203 width=0) (actual time=1.072..9.748 rows=143 loops=1) + -> Index Scan using index_user_interacted_projects_on_user_id on user_interacted_projects (cost=0.43..160.71 rows=205 width=4) (actual time=0.939..2.508 rows=145 loops=1) + Index Cond: (user_id = 1) + -> Index Scan using projects_pkey on projects (cost=0.43..3.45 rows=1 width=4) (actual time=0.049..0.050 rows=1 loops=145) + Index Cond: (id = user_interacted_projects.project_id) + Filter: (visibility_level = ANY ('{0,20}'::integer[])) + Rows Removed by Filter: 0 + Planning time: 2.614 ms + Execution time: 9.809 ms +``` + +Here it only took us just under 10 milliseconds to get the data. We can also see +we're retrieving far fewer projects: + +``` +Index Scan using projects_pkey on projects (cost=0.43..3.45 rows=1 width=4) (actual time=0.049..0.050 rows=1 loops=145) + Index Cond: (id = user_interacted_projects.project_id) + Filter: (visibility_level = ANY ('{0,20}'::integer[])) + Rows Removed by Filter: 0 +``` + +Here we see we perform 145 loops (`loops=145`), with every loop producing 1 row +(`rows=1`). This is much less than before, and our query performs much better! + +If we look at the plan we also see our costs are very low: + +``` +Index Scan using projects_pkey on projects (cost=0.43..3.45 rows=1 width=4) (actual time=0.049..0.050 rows=1 loops=145) +``` + +Here our cost is only 3.45, and it only takes us 0.050 milliseconds to do so. +The next index scan is a bit more expensive: + +``` +Index Scan using index_user_interacted_projects_on_user_id on user_interacted_projects (cost=0.43..160.71 rows=205 width=4) (actual time=0.939..2.508 rows=145 loops=1) +``` + +Here the cost is 160.71 (`cost=0.43..160.71`), taking about 2.5 milliseconds +(based on the output of `actual time=....`). + +The most expensive part here is the "Nested Loop" that acts upon the result of +these two index scans: + +``` +Nested Loop (cost=0.86..870.52 rows=203 width=0) (actual time=1.072..9.748 rows=143 loops=1) +``` + +Here we had to perform 870.52 disk page fetches for 203 rows, 9.748 +milliseconds, producing 143 rows in a single loop. + +The key takeaway here is that sometimes you have to rewrite (parts of) a query +to make it better. Sometimes that means having to slightly change your feature +to accommodate for better performance. + +## What makes a bad plan + +This is a bit of a difficult question to answer, because the definition of "bad" +is relative to the problem you are trying to solve. However, some patterns are +best avoided in most cases, such as: + +* Sequential scans on large tables +* Filters that remove a lot of rows +* Performing a certain step (e.g. an index scan) that requires _a lot_ of + buffers (e.g. more than 512 MB for GitLab.com). + +As a general guideline, aim for a query that: + +1. Takes no more than 10 milliseconds. Our target time spent in SQL per request + is around 100 milliseconds, so every query should be as fast as possible. +1. Does not use an excessive number of buffers, relative to the workload. For + example, retrieving ten rows shouldn't require 1 GB of buffers. +1. Does not spend a long amount of time performing disk IO operations. The + setting `track_io_timing` must be enabled for this data to be included in the + output of `EXPLAIN ANALYZE`. +1. Applies a `LIMIT` when retrieving rows without aggregating them, such as + `SELECT * FROM users`. +1. Doesn't use a `Filter` to filter out too many rows, especially if the query + does not use a `LIMIT` to limit the number of returned rows. Filters can + usually be removed by adding a (partial) index. + +These are _guidelines_ and not hard requirements, as different needs may require +different queries. The only _rule_ is that you _must always measure_ your query +(preferably using a production-like database) using `EXPLAIN (ANALYZE, BUFFERS)` +and related tools such as: + +* <https://explain.depesz.com/> +* <http://tatiyants.com/postgres-query-plan-visualization/> + +GitLab employees can also use our chatops solution, available in Slack using the +`/chatops` slash command. You can use chatops to get a query plan by running the +following: + +``` +/chatops run explain SELECT COUNT(*) FROM projects WHERE visibility_level IN (0, 20) +``` + +Visualising the plan using <https://explain.depesz.com/> is also supported: + +``` +/chatops run explain --visual SELECT COUNT(*) FROM projects WHERE visibility_level IN (0, 20) +``` + +Quoting the query is not necessary. + +For more information about the available options, run: + +``` +/chatops run explain --help +``` diff --git a/doc/install/installation.md b/doc/install/installation.md index ea01d88d85f..a310f12b29e 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -153,7 +153,7 @@ 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 @@ -494,11 +494,11 @@ Make GitLab start on boot: ### Install Gitaly # Fetch Gitaly source with Git and compile with Go - sudo -u git -H bundle exec rake "gitlab:gitaly:install[/home/git/gitaly]" RAILS_ENV=production + 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,https://example.com/gitaly.git]" RAILS_ENV=production + 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: diff --git a/doc/install/kubernetes/gitlab_omnibus.md b/doc/install/kubernetes/gitlab_omnibus.md index 9aee6b9dc74..c2c8a7a92fd 100644 --- a/doc/install/kubernetes/gitlab_omnibus.md +++ b/doc/install/kubernetes/gitlab_omnibus.md @@ -120,7 +120,7 @@ gitlabConfigStorageSize: 1Gi Ingress routing and SSL are automatically configured within this Chart. An NGINX ingress is provisioned and configured, and will route traffic to any service. SSL certificates are automatically created and configured by [kube-lego](https://github.com/kubernetes/charts/tree/master/stable/kube-lego). > **Note:** -Let's Encrypt limits a single TLD to five certificate requests within a single week. This means that common DNS wildcard services like [nip.io](http://nip.io) and [nip.io](http://nip.io) are unlikely to work. +Let's Encrypt limits a single TLD to five certificate requests within a single week. This means that common DNS wildcard services like [nip.io](http://nip.io) are unlikely to work. ## Installing GitLab using the Helm Chart > **Note:** diff --git a/doc/integration/README.md b/doc/integration/README.md index 54e78bdef54..8a93d4cb84b 100644 --- a/doc/integration/README.md +++ b/doc/integration/README.md @@ -30,7 +30,7 @@ Bitbucket.org account ## Project services -Integration with services such as Campfire, Flowdock, Gemnasium, HipChat, +Integration with services such as Campfire, Flowdock, HipChat, Pivotal Tracker, and Slack are available in the form of a [Project Service][]. [Project Service]: ../user/project/integrations/project_services.md diff --git a/doc/topics/autodevops/index.md b/doc/topics/autodevops/index.md index f5574506595..0474182e324 100644 --- a/doc/topics/autodevops/index.md +++ b/doc/topics/autodevops/index.md @@ -233,6 +233,11 @@ in **Admin Area > Settings > Continuous Integration and Deployment**. Doing that all the projects that haven't explicitly set an option will have Auto DevOps enabled by default. +NOTE: **Note:** +There is also a feature flag to enable Auto DevOps to a percentage of projects +which can be enabled from the console with +`Feature.get(:force_autodevops_on_by_default).enable_percentage_of_actors(10)`. + ### Deployment strategy > [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/38542) in GitLab 11.0. diff --git a/doc/user/admin_area/img/cohorts.png b/doc/user/admin_area/img/cohorts.png Binary files differdeleted file mode 100644 index 8bae7faff07..00000000000 --- a/doc/user/admin_area/img/cohorts.png +++ /dev/null diff --git a/doc/user/admin_area/monitoring/convdev.md b/doc/user/admin_area/monitoring/convdev.md index a98602c4d70..6ad8a5a7ff0 100644 --- a/doc/user/admin_area/monitoring/convdev.md +++ b/doc/user/admin_area/monitoring/convdev.md @@ -1,29 +1,5 @@ -# Conversational Development Index +--- +redirect_to: '../../instance_statistics/convdev.md' +--- -> [Introduced][ce-30469] in GitLab 9.3. - -Conversational Development Index (ConvDev) gives you an overview of your entire -instance's feature usage, from idea to production. It looks at your usage in the -past 30 days, averaged over the number of active users in that time period. It also -provides a lead score per feature, which is calculated based on GitLab's analysis -of top performing instances, based on [usage ping data][ping] that GitLab has -collected. Your score is compared to the lead score, expressed as a percentage. -The overall index score is an average over all your feature scores. - -![ConvDev index](img/convdev_index.png) - -The page also provides helpful links to articles and GitLab docs, to help you -improve your scores. - -Your GitLab instance's usage ping must be activated in order to use this feature. -Usage ping data is aggregated on GitLab's servers for analysis. Your usage -information is **not sent** to any other GitLab instances. - -If you have just started using GitLab, it may take a few weeks for data to be -collected before this feature is available. - -This feature is accessible only to a system admin, at -**Admin area > Overview > ConvDev Index**. - -[ce-30469]: https://gitlab.com/gitlab-org/gitlab-ce/issues/30469 -[ping]: ../settings/usage_statistics.md#usage-ping +This document was moved to [another location](../../instance_statistics/convdev.md). diff --git a/doc/user/admin_area/monitoring/img/convdev_index.png b/doc/user/admin_area/monitoring/img/convdev_index.png Binary files differdeleted file mode 100644 index 1bf1d6a83c9..00000000000 --- a/doc/user/admin_area/monitoring/img/convdev_index.png +++ /dev/null diff --git a/doc/user/admin_area/settings/continuous_integration.md b/doc/user/admin_area/settings/continuous_integration.md index eb6f915f3f4..76d9a4ceb03 100644 --- a/doc/user/admin_area/settings/continuous_integration.md +++ b/doc/user/admin_area/settings/continuous_integration.md @@ -21,8 +21,9 @@ that this setting is set for each job. The default expiration time of the [job artifacts][art-yml] can be set in the Admin area of your GitLab instance. The syntax of duration is described in [artifacts:expire_in][duration-syntax]. The default is `30 days`. Note that -this setting is set for each job. Set it to 0 if you don't want default -expiration. +this setting is set for each job. Set it to `0` if you don't want default +expiration. The default unit is in seconds. + 1. Go to **Admin area > Settings** (`/admin/application_settings`). diff --git a/doc/user/admin_area/settings/usage_statistics.md b/doc/user/admin_area/settings/usage_statistics.md index 381efdf5d67..b7427592e10 100644 --- a/doc/user/admin_area/settings/usage_statistics.md +++ b/doc/user/admin_area/settings/usage_statistics.md @@ -6,7 +6,7 @@ to perform various actions. All statistics are opt-out, you can enable/disable them from the admin panel under **Admin area > Settings > Usage statistics**. -## Version check +## Version check **[CORE ONLY]** If enabled, version check will inform you if a new version is available and the importance of it through a status. This is shown on the help page (i.e. `/help`) @@ -29,7 +29,7 @@ secure. If you disable version check, this information will not be collected. Enable or disable the version check at **Admin area > Settings > Usage statistics**. -## Usage ping +## Usage ping **[CORE ONLY]** > [Introduced][ee-557] in GitLab Enterprise Edition 8.10. More statistics [were added][ee-735] in GitLab Enterprise Edition @@ -67,6 +67,15 @@ production: &base usage_ping_enabled: false ``` +## Instance statistics visibility **[CORE ONLY]** + +Once usage ping is enabled, GitLab will gather data from other instances and +will be able to show [usage statistics](../../instance_statistics/index.md) +of your instance to your users. + +This can be restricted to admins by selecting "Only admins" in the Instance +Statistics visibility section under **Admin area > Settings > Usage statistics**. + [ee-557]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/557 [ee-735]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/735 [ce-23361]: https://gitlab.com/gitlab-org/gitlab-ce/issues/23361 diff --git a/doc/user/admin_area/user_cohorts.md b/doc/user/admin_area/user_cohorts.md index e25e7a8bbc3..21e61e2ec44 100644 --- a/doc/user/admin_area/user_cohorts.md +++ b/doc/user/admin_area/user_cohorts.md @@ -1,37 +1,5 @@ -# Cohorts +--- +redirect_to: '../instance_statistics/user_cohorts.md' +--- -> **Notes:** -> [Introduced][ce-23361] in GitLab 9.1. - -As a benefit of having the [usage ping active](settings/usage_statistics.md), -GitLab lets you analyze the users' activities of your GitLab installation. -Under `/admin/cohorts`, when the usage ping is active, GitLab will show the -monthly cohorts of new users and their activities over time. - -## Overview - -How do we read the user cohorts table? Let's take an example with the following -user cohorts. - -![User cohort example](img/cohorts.png) - -For the cohort of June 2016, 163 users have been added on this server and have -been active since this month. One month later, in July 2016, out of -these 163 users, 155 users (or 95% of the June cohort) are still active. Two -months later, 139 users (or 85%) are still active. 9 months later, we can see -that only 6% of this cohort are still active. - -The Inactive users column shows the number of users who have been added during -the month, but who have never actually had any activity in the instance. - -How do we measure the activity of users? GitLab considers a user active if: - -* the user signs in -* the user has Git activity (whether push or pull). - -## Setup - -1. [Activate the usage ping](settings/usage_statistics.md) -2. Go to `/admin/cohorts` to see the user cohorts of the server - -[ce-23361]: https://gitlab.com/gitlab-org/gitlab-ce/issues/23361 +This document was moved to [another location](../instance_statistics/user_cohorts.md). diff --git a/doc/user/index.md b/doc/user/index.md index 90f0e2285c3..649c0b664a5 100644 --- a/doc/user/index.md +++ b/doc/user/index.md @@ -172,3 +172,7 @@ Automate GitLab via [API](../api/README.md). ## Git and GitLab Learn what is [Git](../topics/git/index.md) and its best practices. + +## Instance statistics + +See [various statistics](instance_statistics/index.md) of your GitLab instance. diff --git a/doc/user/instance_statistics/convdev.md b/doc/user/instance_statistics/convdev.md new file mode 100644 index 00000000000..d2795e092fc --- /dev/null +++ b/doc/user/instance_statistics/convdev.md @@ -0,0 +1,26 @@ +# Conversational Development Index + +> [Introduced][ce-30469] in GitLab 9.3. + +Conversational Development Index (ConvDev) gives you an overview of your entire +instance's feature usage, from idea to production. It looks at your usage in the +past 30 days, averaged over the number of active users in that time period. It also +provides a lead score per feature, which is calculated based on GitLab's analysis +of top performing instances, based on [usage ping data][ping] that GitLab has +collected. Your score is compared to the lead score, expressed as a percentage. +The overall index score is an average over all your feature scores. + +![ConvDev index](img/convdev_index.png) + +The page also provides helpful links to articles and GitLab docs, to help you +improve your scores. + +Your GitLab instance's usage ping must be activated in order to use this feature. +Usage ping data is aggregated on GitLab's servers for analysis. Your usage +information is **not sent** to any other GitLab instances. + +If you have just started using GitLab, it may take a few weeks for data to be +collected before this feature is available. + +[ce-30469]: https://gitlab.com/gitlab-org/gitlab-ce/issues/30469 +[ping]: ../admin_area/settings/usage_statistics.md#usage-ping diff --git a/doc/user/instance_statistics/img/cohorts.png b/doc/user/instance_statistics/img/cohorts.png Binary files differnew file mode 100644 index 00000000000..12e839e7cd2 --- /dev/null +++ b/doc/user/instance_statistics/img/cohorts.png diff --git a/doc/user/instance_statistics/img/convdev_index.png b/doc/user/instance_statistics/img/convdev_index.png Binary files differnew file mode 100644 index 00000000000..191295c918b --- /dev/null +++ b/doc/user/instance_statistics/img/convdev_index.png diff --git a/doc/user/instance_statistics/img/instance_statistics_button.png b/doc/user/instance_statistics/img/instance_statistics_button.png Binary files differnew file mode 100644 index 00000000000..6104321b1a6 --- /dev/null +++ b/doc/user/instance_statistics/img/instance_statistics_button.png diff --git a/doc/user/instance_statistics/index.md b/doc/user/instance_statistics/index.md new file mode 100644 index 00000000000..a4eca89b7fe --- /dev/null +++ b/doc/user/instance_statistics/index.md @@ -0,0 +1,19 @@ +# Instance statistics + +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/41416) +in GitLab 11.2. + +Instance statistics gives users or admins access to instance-wide analytics. +They are accessible to all users by default (GitLab admins can restrict its +visibility in the [admin area](../admin_area/settings/usage_statistics.md)), +and can be accessed via the top bar. + +![Instance Statistics button](img/instance_statistics_button.png) + +For the statistics to show up, [usage ping must be enabled](../admin_area/settings/usage_statistics.md#usage-ping) +by an admin in the admin settings area. + +There are two kinds of statistics: + +- [Conversational Development (ConvDev) Index](convdev.md): Provides an overview of your entire instance's feature usage. +- [User Cohorts](user_cohorts.md): Display the monthly cohorts of new users and their activities over time. diff --git a/doc/user/instance_statistics/user_cohorts.md b/doc/user/instance_statistics/user_cohorts.md new file mode 100644 index 00000000000..70d5912dc4e --- /dev/null +++ b/doc/user/instance_statistics/user_cohorts.md @@ -0,0 +1,27 @@ +# Cohorts + +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/23361) +in GitLab 9.1. + +As a benefit of having the [usage ping active](../admin_area/settings/usage_statistics.md), +GitLab lets you analyze the users' activities over time of your GitLab installation. + +## Overview + +How do we read the user cohorts table? Let's take an example with the following +user cohorts. + +![User cohort example](img/cohorts.png) + +For the cohort of Jan 2018, 15 users have been added on this server and have +been active since this month. One month later, in Feb 2018, all 15 users are +still active. 6 months later (Month 6, July), we can see 10 users from this cohort +are active, or 66% of the original cohort of 15 that joined in January. + +The Inactive users column shows the number of users who have been added during +the month, but who have never actually had any activity in the instance. + +How do we measure the activity of users? GitLab considers a user active if: + +* the user signs in +* the user has Git activity (whether push or pull). diff --git a/doc/user/markdown.md b/doc/user/markdown.md index bd199b55a61..6203561265b 100644 --- a/doc/user/markdown.md +++ b/doc/user/markdown.md @@ -234,6 +234,13 @@ https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md#emoji Consult the [Emoji Cheat Sheet](https://www.emojicopy.com) for a list of all supported emoji codes. :thumbsup: + Most emoji are natively supported on macOS, Windows, iOS, Android and will fallback to image-based emoji where there is lack of support. + + On Linux, you can download [Noto Color Emoji](https://www.google.com/get/noto/help/emoji/) to get full native emoji support. + + Ubuntu 18.04 (like many modern Linux distros) has this font installed by default. + + Sometimes you want to :monkey: around a bit and add some :star2: to your :speech_balloon:. Well we have a gift for you: :zap: You can use emoji anywhere GFM is supported. :v: @@ -244,6 +251,14 @@ If you are new to this, don't be :fearful:. You can easily join the emoji :famil Consult the [Emoji Cheat Sheet](https://www.emojicopy.com) for a list of all supported emoji codes. :thumbsup: +Most emoji are natively supported on macOS, Windows, iOS, Android and will fallback to image-based emoji where there is lack of support. + +On Linux, you can download [Noto Color Emoji](https://www.google.com/get/noto/help/emoji/) to get full native emoji support. + +Ubuntu 18.04 (like many modern Linux distros) has this font installed by default. + + + ### Special GitLab References GFM recognizes special references. @@ -259,6 +274,7 @@ GFM will recognize the following: | `@user_name` | specific user | | `@group_name` | specific group | | `@all` | entire team | +| `namespace/project>` | project | | `#12345` | issue | | `!123` | merge request | | `$123` | snippet | diff --git a/doc/user/project/img/issue_board.png b/doc/user/project/img/issue_board.png Binary files differindex 50e051e25a0..925b969eebe 100644 --- a/doc/user/project/img/issue_board.png +++ b/doc/user/project/img/issue_board.png diff --git a/doc/user/project/import/img/manifest_upload.png b/doc/user/project/import/img/manifest_upload.png Binary files differdeleted file mode 100644 index d6bf4b157dd..00000000000 --- a/doc/user/project/import/img/manifest_upload.png +++ /dev/null diff --git a/doc/user/project/import/index.md b/doc/user/project/import/index.md index b55435e5b4f..4ea35a30bbf 100644 --- a/doc/user/project/import/index.md +++ b/doc/user/project/import/index.md @@ -11,7 +11,7 @@ 1. [From SVN](svn.md) 1. [From TFS](tfs.md) 1. [From repo by URL](repo_by_url.md) -1. [By uploading a manifest file](manifest.md) +1. [By uploading a manifest file (AOSP)](manifest.md) In addition to the specific migration documentation above, you can import any Git repository via HTTP from the New Project page. Be aware that if the diff --git a/doc/user/project/import/manifest.md b/doc/user/project/import/manifest.md index 06171f11e12..24bf6541a9d 100644 --- a/doc/user/project/import/manifest.md +++ b/doc/user/project/import/manifest.md @@ -1,36 +1,31 @@ # Import multiple repositories by uploading a manifest file -GitLab allows you to import all the required git repositories -based a manifest file like the one used by the [Android repository](https://android.googlesource.com/platform/manifest/+/2d6f081a3b05d8ef7a2b1b52b0d536b2b74feab4/default.xml). -This feature can be very handy when you need to import a project with many repositories like Android Open Source Project (AOSP). +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/28811) in +GitLab 11.2. +GitLab allows you to import all the required Git repositories +based on a manifest file like the one used by the +[Android repository](https://android.googlesource.com/platform/manifest/+/2d6f081a3b05d8ef7a2b1b52b0d536b2b74feab4/default.xml). +This feature can be very handy when you need to import a project with many +repositories like the Android Open Source Project (AOSP). ->**Note:** -This feature requires [subgroups](../../group/subgroups/index.md) to be supported by your database. +## Requirements -You can do it by following next steps: +GitLab must be using PostgreSQL for its database, since +[subgroups](../../group/subgroups/index.md) are needed for the manifest import +to work. -1. From your GitLab dashboard click **New project** -1. Switch to the **Import project** tab -1. Click on the **Manifest file** button -1. Provide GitLab with a manifest xml file -1. Select a group you want to import to (you need to create a group first if you don't have one) -1. Click **List available repositories** -1. You will be redirected to the import status page with projects list based on manifest file -1. Check the list and click 'Import all repositories' to start import. - -![Manifest upload](img/manifest_upload.png) +Read more about the [database requirements](../../../install/requirements.md#database). -![Manifest status](img/manifest_status.png) +## Manifest format -### Manifest format +A manifest must be an XML file. There must be one `remote` tag with a `review` +attribute that contains a URL to a Git server, and each `project` tag must have +a `name` and `path` attribute. GitLab will then build the URL to the repository +by combining the URL from the `remote` tag with a project name. +A path attribute will be used to represent the project path in GitLab. -A manifest must be an XML file. There must be one `remote` tag with `review` attribute -that contains a URL to a git server. Each `project` tag must have `name` and `path` attribute. -GitLab will build URL to the repository by combining URL from `remote` tag with a project name. -A path attribute will be used to represent project path in GitLab system. - -Below is a valid example of manifest file. +Below is a valid example of a manifest file: ```xml <manifest> @@ -41,9 +36,24 @@ Below is a valid example of manifest file. </manifest> ``` -As result next projects will be created: +As a result, the following projects will be created: | GitLab | Import URL | |---|---| -| https://gitlab/YOUR_GROUP/build/make | https://android-review.googlesource.com/platform/build | -| https://gitlab/YOUR_GROUP/build/blueprint | https://android-review.googlesource.com/platform/build/blueprint | +| https://gitlab.com/YOUR_GROUP/build/make | https://android-review.googlesource.com/platform/build | +| https://gitlab.com/YOUR_GROUP/build/blueprint | https://android-review.googlesource.com/platform/build/blueprint | + +## Importing the repositories + +You can start the import with: + +1. From your GitLab dashboard click **New project** +1. Switch to the **Import project** tab +1. Click on the **Manifest file** button +1. Provide GitLab with a manifest xml file +1. Select a group you want to import to (you need to create a group first if you don't have one) +1. Click **List available repositories**. At this point, you will be redirected + to the import status page with projects list based on the manifest file. +1. Check the list and click **Import all repositories** to start the import. + + ![Manifest status](img/manifest_status.png) diff --git a/doc/user/project/integrations/hangouts_chat.md b/doc/user/project/integrations/hangouts_chat.md index 6ab44420a10..47525617d95 100644 --- a/doc/user/project/integrations/hangouts_chat.md +++ b/doc/user/project/integrations/hangouts_chat.md @@ -1,5 +1,7 @@ # Hangouts Chat service +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/43756) in GitLab 11.2. + The Hangouts Chat service sends notifications from GitLab to the room for which the webhook was created. ## On Hangouts Chat diff --git a/doc/user/project/integrations/project_services.md b/doc/user/project/integrations/project_services.md index 05ee1b4e6d7..efb0381d7aa 100644 --- a/doc/user/project/integrations/project_services.md +++ b/doc/user/project/integrations/project_services.md @@ -34,7 +34,6 @@ Click on the service links to see further configuration instructions and details | [Emails on push](emails_on_push.md) | Email the commits and diff of each push to a list of recipients | | External Wiki | Replaces the link to the internal wiki with a link to an external wiki | | Flowdock | Flowdock is a collaboration web app for technical teams | -| Gemnasium _(Has been deprecated in GitLab 11.0)_ | Gemnasium monitors your project dependencies and alerts you about updates and security vulnerabilities | | [Hangouts Chat](hangouts_chat.md) | Receive events notifications in Google Hangouts Chat | | [HipChat](hipchat.md) | Private group chat and IM | | [Irker (IRC gateway)](irker.md) | Send IRC messages, on update, to a list of recipients through an Irker gateway | diff --git a/doc/user/project/integrations/webhooks.md b/doc/user/project/integrations/webhooks.md index 8e486318980..770b1810da1 100644 --- a/doc/user/project/integrations/webhooks.md +++ b/doc/user/project/integrations/webhooks.md @@ -1102,6 +1102,7 @@ X-Gitlab-Event: Build Hook "build_finished_at": null, "build_duration": null, "build_allow_failure": false, + "build_failure_reason": "script_failure", "project_id": 380, "project_name": "gitlab-org/gitlab-test", "user": { @@ -1122,7 +1123,6 @@ X-Gitlab-Event: Build Hook }, "repository": { "name": "gitlab_test", - "git_ssh_url": "git@192.168.64.1:gitlab-org/gitlab-test.git", "description": "Atque in sunt eos similique dolores voluptatem.", "homepage": "http://192.168.64.1:3005/gitlab-org/gitlab-test", "git_ssh_url": "git@192.168.64.1:gitlab-org/gitlab-test.git", diff --git a/doc/user/project/issue_board.md b/doc/user/project/issue_board.md index 49b49271cff..0e847be79c2 100644 --- a/doc/user/project/issue_board.md +++ b/doc/user/project/issue_board.md @@ -119,10 +119,10 @@ Issue Board, that is, create or delete lists and drag issues from one list to an ## Issue Board terminology - **Issue Board** - Each board represents a unique view for your issues. It can have multiple lists with each list consisting of issues represented by cards. -- **List** - A column on the issue board that displays issues matching certain attributes. In addition to the default lists of 'Backlog' and 'Closed' issue, each additional list will show issues matching your chosen label or assignee. On the top of that list you can see the number of issues that belong to it. +- **List** - A column on the issue board that displays issues matching certain attributes. In addition to the default lists of 'Open' and 'Closed' issue, each additional list will show issues matching your chosen label or assignee. On the top of that list you can see the number of issues that belong to it. - **Label list**: a list based on a label. It shows all opened issues with that label. - **Assignee list**: a list which includes all issues assigned to a user. - - **Backlog** (default): shows all open issues that do not belong to one of the other lists. Always appears as the leftmost list. + - **Open** (default): shows all open issues that do not belong to one of the other lists. Always appears as the leftmost list. - **Closed** (default): shows all closed issues. Always appears as the rightmost list. - **Card** - A box in the list that represents an individual issue. The information you can see on a card consists of the issue number, the issue title, the assignee, and the labels associated with the issue. You can drag cards from one list to another to change their label or assignee from that of the source list to that of the destination list. @@ -353,9 +353,9 @@ To remove an assignee list, just as with a label list, click the trash icon. When dragging issues between lists, different behavior occurs depending on the source list and the target list. -| | To Backlog | To Closed | To label `B` list | To assignee `Bob` list | +| | To Open | To Closed | To label `B` list | To assignee `Bob` list | | --- | --- | --- | --- | --- | -| From Backlog | - | Issue closed | `B` added | `Bob` assigned | +| From Open | - | Issue closed | `B` added | `Bob` assigned | | From Closed | Issue reopened | - | Issue reopened<br/>`B` added | Issue reopened<br/>`Bob` assigned | | From label `A` list | `A` removed | Issue closed | `A` removed<br/>`B` added | `Bob` assigned | | From assignee `Alice` list | `Alice` unassigned | Issue closed | `B` added | `Alice` unassigned<br/>`Bob` assigned | diff --git a/doc/user/project/merge_requests/index.md b/doc/user/project/merge_requests/index.md index 86ecf33ed31..43ca498d006 100644 --- a/doc/user/project/merge_requests/index.md +++ b/doc/user/project/merge_requests/index.md @@ -43,8 +43,7 @@ A. Consider you are a software developer working in a team: 1. You checkout a new branch, and submit your changes through a merge request 1. You gather feedback from your team -1. You work on the implementation optimizing code with [Code Quality reports](https://docs.gitlab.com/ee/user/project/merge_requests/code_quality.html) **[STARTER]** -1. You build and test your changes with GitLab CI/CD +1. You verify your changes with [JUnit test reports](../../../ci/junit_test_reports.md) in GitLab CI/CD 1. You request the approval from your manager 1. Your manager pushes a commit with his final review, [approves the merge request](https://docs.gitlab.com/ee/user/project/merge_requests/merge_request_approvals.html), and set it to [merge when pipeline succeeds](#merge-when-pipeline-succeeds) (Merge Request Approvals are available in GitLab Starter) 1. Your changes get deployed to production with [manual actions](../../../ci/yaml/README.md#manual-actions) for GitLab CI/CD diff --git a/doc/user/project/pages/getting_started_part_three.md b/doc/user/project/pages/getting_started_part_three.md index 61af1d2ab27..e1eede8bbed 100644 --- a/doc/user/project/pages/getting_started_part_three.md +++ b/doc/user/project/pages/getting_started_part_three.md @@ -1,5 +1,5 @@ --- -last_updated: 2018-02-16 +last_updated: 2018-08-16 author: Marcia Ramos author_gitlab: marcia level: beginner @@ -28,7 +28,7 @@ Let's start from the beginning with [DNS records](#dns-records). If you already know how they work and want to skip the introduction to DNS, you may be interested in skipping it until the [TL;DR](#tl-dr) section below. -## DNS Records +### DNS Records A Domain Name System (DNS) web service routes visitors to websites by translating domain names (such as `www.example.com`) into the @@ -64,22 +64,28 @@ for the most popular hosting services: If your hosting service is not listed above, you can just try to search the web for `how to add dns record on <my hosting service>`. -### DNS A record +#### DNS A record In case you want to point a root domain (`example.com`) to your GitLab Pages site, deployed to `namespace.gitlab.io`, you need to log into your domain's admin control panel and add a DNS `A` record pointing your domain to Pages' server IP address. For projects on -GitLab.com, this IP is `52.167.214.135`. For projects living in +GitLab.com, this IP is `35.185.44.232`. For projects living in other GitLab instances (CE or EE), please contact your sysadmin asking for this information (which IP address is Pages server running on your instance). **Practical Example:** -![DNS A record pointing to GitLab.com Pages server](img/dns_add_new_a_record_example_updated.png) +![DNS A record pointing to GitLab.com Pages server](img/dns_add_new_a_record_example_updated_2018.png) -### DNS CNAME record +NOTE: **Note:** +Note that if you use your root domain for your GitLab Pages website **only**, and if +your domain registrar supports this feature, you can add a DNS apex `CNAME` +record instead of an `A` record. The main advantage of doing so is that when GitLab Pages +IP on GitLab.com changes for whatever reason, you don't need to update your `A` record. + +#### DNS CNAME record In case you want to point a subdomain (`hello-world.example.com`) to your GitLab Pages site initially deployed to `namespace.gitlab.io`, @@ -112,14 +118,14 @@ If the domain has multiple uses (e.g., you host email on it as well): | From | DNS Record | To | | ---- | ---------- | -- | -| domain.com | A | 52.167.214.135 | +| domain.com | A | 35.185.44.232 | | domain.com | TXT | gitlab-pages-verification-code=00112233445566778899aabbccddeeff | If the domain is dedicated to GitLab Pages use and no other services run on it: | From | DNS Record | To | | ---- | ---------- | -- | -| subdomain.domain.com | CNAME | gitlab.io | +| subdomain.domain.com | CNAME | namespace.gitlab.io | | _gitlab-pages-verification-code.subdomain.domain.com | TXT | gitlab-pages-verification-code=00112233445566778899aabbccddeeff | > **Notes**: @@ -129,9 +135,11 @@ If the domain is dedicated to GitLab Pages use and no other services run on it: > - **Do not** add any special chars after the default Pages domain. E.g., **do not** point your `subdomain.domain.com` to `namespace.gitlab.io.` or `namespace.gitlab.io/`. -> - GitLab Pages IP on GitLab.com [has been changed](https://about.gitlab.com/2017/03/06/we-are-changing-the-ip-of-gitlab-pages-on-gitlab-com/) from `104.208.235.32` to `52.167.214.135`. +> - GitLab Pages IP on GitLab.com [was changed](https://about.gitlab.com/2017/03/06/we-are-changing-the-ip-of-gitlab-pages-on-gitlab-com/) in 2017 +> - GitLab Pages IP on GitLab.com [has been changed](https://about.gitlab.com/2018/07/19/gcp-move-update/#gitlab-pages-and-custom-domains) +from `52.167.214.135` to `35.185.44.232` in 2018 -## Add your custom domain to GitLab Pages settings +### Add your custom domain to GitLab Pages settings Once you've set the DNS record, you'll need navigate to your project's **Setting > Pages** and click **+ New domain** to add your custom domain to @@ -165,6 +173,18 @@ will fail and attempts to visit your domain will respond with a 404. Read through the [general documentation on GitLab Pages](introduction.md#add-a-custom-domain-to-your-pages-website) to learn more about adding custom domains to GitLab Pages sites. +### Redirecting `www.domain.com` to `domain.com` with Cloudflare + +If you use Cloudflare, you can redirect `www` to `domain.com` without the need of adding both +`www.domain.com` and `domain.com` to GitLab. This happens due to a [Cloudflare feature that creates +a 301 redirect as a "page rule"](https://gitlab.com/gitlab-org/gitlab-ce/issues/48848#note_87314849) for redirecting `www.domain.com` to `domain.com`. In this case, +you can use the following setup: + +- In Cloudflare, create a DNS `A` record pointing `domain.com` to `35.185.44.232` +- In GitLab, add the domain to GitLab Pages +- In Cloudflare, create a DNS `TXT` record to verify your domain +- In Cloudflare, create a DNS `CNAME` record poiting `www` to `domain.com` + ## SSL/TLS Certificates Every GitLab Pages project on GitLab.com will be available under diff --git a/doc/user/project/pages/img/dns_add_new_a_record_example_updated.png b/doc/user/project/pages/img/dns_add_new_a_record_example_updated.png Binary files differdeleted file mode 100644 index 2661a497b91..00000000000 --- a/doc/user/project/pages/img/dns_add_new_a_record_example_updated.png +++ /dev/null diff --git a/doc/user/project/pages/img/dns_add_new_a_record_example_updated_2018.png b/doc/user/project/pages/img/dns_add_new_a_record_example_updated_2018.png Binary files differnew file mode 100644 index 00000000000..fa72df66587 --- /dev/null +++ b/doc/user/project/pages/img/dns_add_new_a_record_example_updated_2018.png diff --git a/doc/user/project/quick_actions.md b/doc/user/project/quick_actions.md index 0ef8eddad20..8fdfd2a6f4d 100644 --- a/doc/user/project/quick_actions.md +++ b/doc/user/project/quick_actions.md @@ -1,7 +1,7 @@ # GitLab quick actions -Quick actions are textual shortcuts for common actions on issues or merge -requests that are usually done by clicking buttons or dropdowns in GitLab's UI. +Quick actions are textual shortcuts for common actions on issues, merge requests +or commits that are usually done by clicking buttons or dropdowns in GitLab's UI. You can enter these commands while creating a new issue or merge request, and in comments. Each command should be on a separate line in order to be properly detected and executed. The commands are removed from the issue, merge request or @@ -39,7 +39,8 @@ do. | `/board_move ~column` | Move issue to column on the board | | `/duplicate #issue` | Closes this issue and marks it as a duplicate of another issue | | `/move path/to/project` | Moves issue to another project | +| `/tag v1.2.3 <message>` | Tags a commit with a given tag name and optional message | | `/tableflip` | Append the comment with `(╯°□°)╯︵ ┻━┻` | | `/shrug` | Append the comment with `¯\_(ツ)_/¯` | | <code>/copy_metadata #issue | !merge_request</code> | Copy labels and milestone from other issue or merge request | -| `/confidential` | Makes the issue confidential |
\ No newline at end of file +| `/confidential` | Makes the issue confidential | diff --git a/doc/user/project/repository/img/repository_languages.png b/doc/user/project/repository/img/repository_languages.png Binary files differnew file mode 100644 index 00000000000..d9fb1278e06 --- /dev/null +++ b/doc/user/project/repository/img/repository_languages.png diff --git a/doc/user/project/repository/index.md b/doc/user/project/repository/index.md index 704c1777e62..1c3915a5fdd 100644 --- a/doc/user/project/repository/index.md +++ b/doc/user/project/repository/index.md @@ -155,6 +155,16 @@ The repository graph displays visually the Git flow strategy used in that reposi Find it under your project's **Repository > Graph**. +## Repository Languages + +For the default branch of each repository, GitLab will determine what programming languages +were used and display this on the projects pages. + +![Repository Languages bar](img/repository_languages.png) + +Not all files are detected, among others; documentation, +vendored code, and most markup languages are excluded. + ## Compare Select branches to compare and view the changes inline: diff --git a/doc/user/project/web_ide/img/admin_clientside_evaluation.png b/doc/user/project/web_ide/img/admin_clientside_evaluation.png Binary files differnew file mode 100644 index 00000000000..a930490398b --- /dev/null +++ b/doc/user/project/web_ide/img/admin_clientside_evaluation.png diff --git a/doc/user/project/web_ide/img/clientside_evaluation.png b/doc/user/project/web_ide/img/clientside_evaluation.png Binary files differnew file mode 100644 index 00000000000..bd04d3d644b --- /dev/null +++ b/doc/user/project/web_ide/img/clientside_evaluation.png diff --git a/doc/user/project/web_ide/index.md b/doc/user/project/web_ide/index.md index 511ac2d7e79..16969b2c527 100644 --- a/doc/user/project/web_ide/index.md +++ b/doc/user/project/web_ide/index.md @@ -72,5 +72,39 @@ leaving the Web IDE. Click the dropdown in the top of the sidebar to open a list of branches. You will need to commit or discard all your changes before switching to a different branch. +## Client Side Evaluation + +> [Introduced in](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/19764) [GitLab Core][ce] 11.2. + +The Web IDE can be used to preview JavaScript projects right in the browser. +This feature uses CodeSandbox to compile and bundle the JavaScript used to +preview the web application. On public projects, an `Open in CodeSandbox` +button is visible which will transfer the contents of the project into a +CodeSandbox project to share with others. +**Note** this button is not visible on private or internal projects. + +![Web IDE Client Side Evaluation](img/clientside_evaluation.png) + +### Enabling Client Side Evaluation + +The Client Side Evaluation feature needs to be enabled in the GitLab instances +admin settings. Client Side Evaluation is enabled for all projects on +GitLab.com + +![Admin Client Side Evaluation setting](img/admin_clientside_evaluation.png) + +Once it has been enabled in application settings, projects with a +`package.json` file and a `main` entry point can be previewed inside of the Web +IDE. An example `package.json` is below. + +```json +{ + "main": "index.js", + "dependencies": { + "vue": "latest" + } +} +``` + [ce]: https://about.gitlab.com/pricing/ [ee]: https://about.gitlab.com/pricing/ diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 27f28e1df93..b6393fdef19 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -856,7 +856,7 @@ module API class NotificationSetting < Grape::Entity expose :level expose :events, if: ->(notification_setting, _) { notification_setting.custom? } do - ::NotificationSetting::EMAIL_EVENTS.each do |event| + ::NotificationSetting.email_events.each do |event| expose event end end @@ -1080,6 +1080,10 @@ module API expose :filename, :size end + class JobArtifact < Grape::Entity + expose :file_type, :size, :filename, :file_format + end + class JobBasic < Grape::Entity expose :id, :status, :stage, :name, :ref, :tag, :coverage expose :created_at, :started_at, :finished_at @@ -1094,7 +1098,9 @@ module API end class Job < JobBasic + # artifacts_file is included in job_artifacts, but kept for backward compatibility (remove in api/v5) expose :artifacts_file, using: JobArtifactFile, if: -> (job, opts) { job.artifacts? } + expose :job_artifacts, as: :artifacts, using: JobArtifact expose :runner, with: Runner expose :artifacts_expire_at end @@ -1153,7 +1159,7 @@ module API class License < Grape::Entity expose :key, :name, :nickname - expose :featured, as: :popular + expose :popular?, as: :popular expose :url, as: :html_url expose(:source_url) { |license| license.meta['source'] } expose(:description) { |license| license.meta['description'] } diff --git a/lib/api/jobs.rb b/lib/api/jobs.rb index 10c6e565f09..fc8c52085ab 100644 --- a/lib/api/jobs.rb +++ b/lib/api/jobs.rb @@ -38,7 +38,7 @@ module API builds = user_project.builds.order('id DESC') builds = filter_builds(builds, params[:scope]) - builds = builds.preload(:user, :job_artifacts_archive, :runner, pipeline: :project) + builds = builds.preload(:user, :job_artifacts_archive, :job_artifacts, :runner, pipeline: :project) present paginate(builds), with: Entities::Job end @@ -54,7 +54,7 @@ module API pipeline = user_project.pipelines.find(params[:pipeline_id]) builds = pipeline.builds builds = filter_builds(builds, params[:scope]) - builds = builds.preload(:job_artifacts_archive, project: [:namespace]) + builds = builds.preload(:job_artifacts_archive, :job_artifacts, project: [:namespace]) present paginate(builds), with: Entities::Job end diff --git a/lib/api/notification_settings.rb b/lib/api/notification_settings.rb index 0266bf2f717..bf0d6b9e434 100644 --- a/lib/api/notification_settings.rb +++ b/lib/api/notification_settings.rb @@ -23,7 +23,7 @@ module API params do optional :level, type: String, desc: 'The global notification level' optional :notification_email, type: String, desc: 'The email address to send notifications' - NotificationSetting::EMAIL_EVENTS.each do |event| + NotificationSetting.email_events.each do |event| optional event, type: Boolean, desc: 'Enable/disable this notification' end end @@ -50,7 +50,9 @@ module API end end - %w[group project].each do |source_type| + [Group, Project].each do |source_class| + source_type = source_class.name.underscore + params do requires :id, type: String, desc: "The #{source_type} ID" end @@ -73,7 +75,7 @@ module API end params do optional :level, type: String, desc: "The #{source_type} notification level" - NotificationSetting::EMAIL_EVENTS.each do |event| + NotificationSetting.email_events(source_class).each do |event| optional event, type: Boolean, desc: 'Enable/disable this notification' end end diff --git a/lib/api/repositories.rb b/lib/api/repositories.rb index 33a9646ac3b..79736107bbb 100644 --- a/lib/api/repositories.rb +++ b/lib/api/repositories.rb @@ -123,6 +123,39 @@ module API not_found! end end + + desc 'Get the common ancestor between commits' do + success Entities::Commit + end + params do + # For now we just support 2 refs passed, but `merge-base` supports + # multiple defining this as an Array instead of 2 separate params will + # make sure we don't need to deprecate this API in favor of one + # supporting multiple commits when this functionality gets added to + # Gitaly + requires :refs, type: Array[String] + end + get ':id/repository/merge_base' do + refs = params[:refs] + + unless refs.size == 2 + render_api_error!('Provide exactly 2 refs', 400) + end + + merge_base = Gitlab::Git::MergeBase.new(user_project.repository, refs) + + if merge_base.unknown_refs.any? + ref_noun = 'ref'.pluralize(merge_base.unknown_refs.size) + message = "Could not find #{ref_noun}: #{merge_base.unknown_refs.join(', ')}" + render_api_error!(message, 400) + end + + if merge_base.commit + present merge_base.commit, with: Entities::Commit + else + not_found!("Merge Base") + end + end end end end diff --git a/lib/api/services.rb b/lib/api/services.rb index 1f2bf546cd7..d1a5ee7db35 100644 --- a/lib/api/services.rb +++ b/lib/api/services.rb @@ -354,20 +354,6 @@ module API desc: 'Flowdock token' } ], - 'gemnasium' => [ - { - required: true, - name: :api_key, - type: String, - desc: 'Your personal API key on gemnasium.com' - }, - { - required: true, - name: :token, - type: String, - desc: "The project's slug on gemnasium.com" - } - ], 'hangouts-chat' => [ { required: true, @@ -695,7 +681,6 @@ module API EmailsOnPushService, ExternalWikiService, FlowdockService, - GemnasiumService, HangoutsChatService, HipchatService, IrkerService, diff --git a/lib/api/templates.rb b/lib/api/templates.rb index 41862768a3f..927baaea652 100644 --- a/lib/api/templates.rb +++ b/lib/api/templates.rb @@ -16,31 +16,8 @@ module API gitlab_version: 8.15 } }.freeze - PROJECT_TEMPLATE_REGEX = - %r{[\<\{\[] - (project|description| - one\sline\s.+\swhat\sit\sdoes\.) # matching the start and end is enough here - [\>\}\]]}xi.freeze - YEAR_TEMPLATE_REGEX = /[<{\[](year|yyyy)[>}\]]/i.freeze - FULLNAME_TEMPLATE_REGEX = - %r{[\<\{\[] - (fullname|name\sof\s(author|copyright\sowner)) - [\>\}\]]}xi.freeze helpers do - def parsed_license_template - # We create a fresh Licensee::License object since we'll modify its - # content in place below. - template = Licensee::License.new(params[:name]) - - template.content.gsub!(YEAR_TEMPLATE_REGEX, Time.now.year.to_s) - template.content.gsub!(PROJECT_TEMPLATE_REGEX, params[:project]) if params[:project].present? - - fullname = params[:fullname].presence || current_user.try(:name) - template.content.gsub!(FULLNAME_TEMPLATE_REGEX, fullname) if fullname - template - end - def render_response(template_type, template) not_found!(template_type.to_s.singularize) unless template present template, with: Entities::Template @@ -56,11 +33,12 @@ module API use :pagination end get "templates/licenses" do - options = { - featured: declared(params)[:popular].present? ? true : nil - } - licences = ::Kaminari.paginate_array(Licensee::License.all(options)) - present paginate(licences), with: Entities::License + popular = declared(params)[:popular] + popular = to_boolean(popular) if popular.present? + + templates = LicenseTemplateFinder.new(popular: popular).execute + + present paginate(::Kaminari.paginate_array(templates)), with: ::API::Entities::License end desc 'Get the text for a specific license' do @@ -71,9 +49,15 @@ module API requires :name, type: String, desc: 'The name of the template' end get "templates/licenses/:name", requirements: { name: /[\w\.-]+/ } do - not_found!('License') unless Licensee::License.find(declared(params)[:name]) + templates = LicenseTemplateFinder.new.execute + template = templates.find { |template| template.key == params[:name] } + + not_found!('License') unless template.present? - template = parsed_license_template + template.resolve!( + project_name: params[:project].presence, + fullname: params[:fullname].presence || current_user&.name + ) present template, with: ::API::Entities::License end diff --git a/lib/banzai/filter/project_reference_filter.rb b/lib/banzai/filter/project_reference_filter.rb new file mode 100644 index 00000000000..83cf45097ed --- /dev/null +++ b/lib/banzai/filter/project_reference_filter.rb @@ -0,0 +1,117 @@ +# frozen_string_literal: true + +module Banzai + module Filter + # HTML filter that replaces project references with links. + class ProjectReferenceFilter < ReferenceFilter + self.reference_type = :project + + # Public: Find `namespace/project>` project references in text + # + # ProjectReferenceFilter.references_in(text) do |match, project| + # "<a href=...>#{project}></a>" + # end + # + # text - String text to search. + # + # Yields the String match, and the String project name. + # + # Returns a String replaced with the return of the block. + def self.references_in(text) + text.gsub(Project.markdown_reference_pattern) do |match| + yield match, "#{$~[:namespace]}/#{$~[:project]}" + end + end + + def call + ref_pattern = Project.markdown_reference_pattern + ref_pattern_start = /\A#{ref_pattern}\z/ + + nodes.each do |node| + if text_node?(node) + replace_text_when_pattern_matches(node, ref_pattern) do |content| + project_link_filter(content) + end + elsif element_node?(node) + yield_valid_link(node) do |link, inner_html| + if link =~ ref_pattern_start + replace_link_node_with_href(node, link) do + project_link_filter(link, link_content: inner_html) + end + end + end + end + end + + doc + end + + # Replace `namespace/project>` project references in text with links to the referenced + # project page. + # + # text - String text to replace references in. + # link_content - Original content of the link being replaced. + # + # Returns a String with `namespace/project>` references replaced with links. All links + # have `gfm` and `gfm-project` class names attached for styling. + def project_link_filter(text, link_content: nil) + self.class.references_in(text) do |match, project_path| + cached_call(:banzai_url_for_object, match, path: [Project, project_path.downcase]) do + if project = projects_hash[project_path.downcase] + link_to_project(project, link_content: link_content) || match + else + match + end + end + end + end + + # Returns a Hash containing all Project objects for the project + # references in the current document. + # + # The keys of this Hash are the project paths, the values the + # corresponding Project objects. + def projects_hash + @projects ||= Project.eager_load(:route, namespace: [:route]) + .where_full_path_in(projects) + .index_by(&:full_path) + .transform_keys(&:downcase) + end + + # Returns all projects referenced in the current document. + def projects + refs = Set.new + + nodes.each do |node| + node.to_html.scan(Project.markdown_reference_pattern) do + refs << "#{$~[:namespace]}/#{$~[:project]}" + end + end + + refs.to_a + end + + private + + def urls + Gitlab::Routing.url_helpers + end + + def link_class + reference_class(:project) + end + + def link_to_project(project, link_content: nil) + url = urls.project_url(project, only_path: context[:only_path]) + data = data_attribute(project: project.id) + content = link_content || project.to_reference_with_postfix + + link_tag(url, data, content, project.name) + end + + def link_tag(url, data, link_content, title) + %(<a href="#{url}" #{data} class="#{link_class}" title="#{escape_once(title)}">#{link_content}</a>) + end + end + end +end diff --git a/lib/banzai/pipeline/gfm_pipeline.rb b/lib/banzai/pipeline/gfm_pipeline.rb index 5dab80dd3eb..e9be05e174e 100644 --- a/lib/banzai/pipeline/gfm_pipeline.rb +++ b/lib/banzai/pipeline/gfm_pipeline.rb @@ -36,6 +36,7 @@ module Banzai def self.reference_filters [ Filter::UserReferenceFilter, + Filter::ProjectReferenceFilter, Filter::IssueReferenceFilter, Filter::ExternalIssueReferenceFilter, Filter::MergeRequestReferenceFilter, diff --git a/lib/banzai/reference_parser/project_parser.rb b/lib/banzai/reference_parser/project_parser.rb new file mode 100644 index 00000000000..b4e3a55b4f1 --- /dev/null +++ b/lib/banzai/reference_parser/project_parser.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +module Banzai + module ReferenceParser + class ProjectParser < BaseParser + include Gitlab::Utils::StrongMemoize + + self.reference_type = :project + + def references_relation + Project + end + + private + + # Returns an Array of Project ids that can be read by the given user. + # + # user - The User for which to check the projects + def readable_project_ids_for(user) + @project_ids_by_user ||= {} + @project_ids_by_user[user] ||= + Project.public_or_visible_to_user(user).where("projects.id IN (?)", @projects_for_nodes.values.map(&:id)).pluck(:id) + end + + def can_read_reference?(user, ref_project, node) + readable_project_ids_for(user).include?(ref_project.try(:id)) + end + end + end +end diff --git a/lib/gitlab/data_builder/build.rb b/lib/gitlab/data_builder/build.rb index 2f1445a050a..0b71b31a476 100644 --- a/lib/gitlab/data_builder/build.rb +++ b/lib/gitlab/data_builder/build.rb @@ -28,6 +28,7 @@ module Gitlab build_finished_at: build.finished_at, build_duration: build.duration, build_allow_failure: build.allow_failure, + build_failure_reason: build.failure_reason, # TODO: do we still need it? project_id: project.id, diff --git a/lib/gitlab/database/migration_helpers.rb b/lib/gitlab/database/migration_helpers.rb index f39b3b6eb5b..7f012312819 100644 --- a/lib/gitlab/database/migration_helpers.rb +++ b/lib/gitlab/database/migration_helpers.rb @@ -58,7 +58,6 @@ module Gitlab if Database.postgresql? options = options.merge({ algorithm: :concurrently }) - disable_statement_timeout end if index_exists?(table_name, column_name, options) @@ -66,7 +65,9 @@ module Gitlab return end - add_index(table_name, column_name, options) + disable_statement_timeout do + add_index(table_name, column_name, options) + end end # Removes an existed index, concurrently when supported @@ -87,7 +88,6 @@ module Gitlab if supports_drop_index_concurrently? options = options.merge({ algorithm: :concurrently }) - disable_statement_timeout end unless index_exists?(table_name, column_name, options) @@ -95,7 +95,9 @@ module Gitlab return end - remove_index(table_name, options.merge({ column: column_name })) + disable_statement_timeout do + remove_index(table_name, options.merge({ column: column_name })) + end end # Removes an existing index, concurrently when supported @@ -116,7 +118,6 @@ module Gitlab if supports_drop_index_concurrently? options = options.merge({ algorithm: :concurrently }) - disable_statement_timeout end unless index_exists_by_name?(table_name, index_name) @@ -124,7 +125,9 @@ module Gitlab return end - remove_index(table_name, options.merge({ name: index_name })) + disable_statement_timeout do + remove_index(table_name, options.merge({ name: index_name })) + end end # Only available on Postgresql >= 9.2 @@ -171,8 +174,6 @@ module Gitlab on_delete = 'SET NULL' if on_delete == :nullify end - disable_statement_timeout - key_name = concurrent_foreign_key_name(source, column) unless foreign_key_exists?(source, target, column: column) @@ -199,7 +200,9 @@ module Gitlab # while running. # # Note this is a no-op in case the constraint is VALID already - execute("ALTER TABLE #{source} VALIDATE CONSTRAINT #{key_name};") + disable_statement_timeout do + execute("ALTER TABLE #{source} VALIDATE CONSTRAINT #{key_name};") + end end def foreign_key_exists?(source, target = nil, column: nil) @@ -224,8 +227,48 @@ module Gitlab # Long-running migrations may take more than the timeout allowed by # the database. Disable the session's statement timeout to ensure # migrations don't get killed prematurely. (PostgreSQL only) + # + # There are two possible ways to disable the statement timeout: + # + # - Per transaction (this is the preferred and default mode) + # - Per connection (requires a cleanup after the execution) + # + # When using a per connection disable statement, code must be inside + # a block so we can automatically execute `RESET ALL` after block finishes + # otherwise the statement will still be disabled until connection is dropped + # or `RESET ALL` is executed def disable_statement_timeout - execute('SET statement_timeout TO 0') if Database.postgresql? + # bypass disabled_statement logic when not using postgres, but still execute block when one is given + unless Database.postgresql? + if block_given? + yield + end + + return + end + + if block_given? + begin + execute('SET statement_timeout TO 0') + + yield + ensure + execute('RESET ALL') + end + else + unless transaction_open? + raise <<~ERROR + Cannot call disable_statement_timeout() without a transaction open or outside of a transaction block. + If you don't want to use a transaction wrap your code in a block call: + + disable_statement_timeout { # code that requires disabled statement here } + + This will make sure statement_timeout is disabled before and reset after the block execution is finished. + ERROR + end + + execute('SET LOCAL statement_timeout TO 0') + end end def true_value @@ -367,30 +410,30 @@ module Gitlab 'in the body of your migration class' end - disable_statement_timeout - - transaction do - if limit - add_column(table, column, type, default: nil, limit: limit) - else - add_column(table, column, type, default: nil) + disable_statement_timeout do + transaction do + if limit + add_column(table, column, type, default: nil, limit: limit) + else + add_column(table, column, type, default: nil) + end + + # Changing the default before the update ensures any newly inserted + # rows already use the proper default value. + change_column_default(table, column, default) end - # Changing the default before the update ensures any newly inserted - # rows already use the proper default value. - change_column_default(table, column, default) - end - - begin - update_column_in_batches(table, column, default, &block) + begin + update_column_in_batches(table, column, default, &block) - change_column_null(table, column, false) unless allow_null - # We want to rescue _all_ exceptions here, even those that don't inherit - # from StandardError. - rescue Exception => error # rubocop: disable all - remove_column(table, column) + change_column_null(table, column, false) unless allow_null + # We want to rescue _all_ exceptions here, even those that don't inherit + # from StandardError. + rescue Exception => error # rubocop: disable all + remove_column(table, column) - raise error + raise error + end end end diff --git a/lib/gitlab/git.rb b/lib/gitlab/git.rb index 55236a1122f..2913a3e416d 100644 --- a/lib/gitlab/git.rb +++ b/lib/gitlab/git.rb @@ -10,9 +10,11 @@ module Gitlab TAG_REF_PREFIX = "refs/tags/".freeze BRANCH_REF_PREFIX = "refs/heads/".freeze - CommandError = Class.new(StandardError) - CommitError = Class.new(StandardError) - OSError = Class.new(StandardError) + BaseError = Class.new(StandardError) + CommandError = Class.new(BaseError) + CommitError = Class.new(BaseError) + OSError = Class.new(BaseError) + UnknownRef = Class.new(BaseError) class << self include Gitlab::EncodingHelper diff --git a/lib/gitlab/git/diff.rb b/lib/gitlab/git/diff.rb index b58296375ef..61ce10ca131 100644 --- a/lib/gitlab/git/diff.rb +++ b/lib/gitlab/git/diff.rb @@ -226,6 +226,7 @@ module Gitlab @new_file = diff.from_id == BLANK_SHA @renamed_file = diff.from_path != diff.to_path @deleted_file = diff.to_id == BLANK_SHA + @too_large = diff.too_large if diff.respond_to?(:too_large) collapse! if diff.respond_to?(:collapsed) && diff.collapsed end diff --git a/lib/gitlab/git/merge_base.rb b/lib/gitlab/git/merge_base.rb new file mode 100644 index 00000000000..b27f7038c26 --- /dev/null +++ b/lib/gitlab/git/merge_base.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +module Gitlab + module Git + class MergeBase + include Gitlab::Utils::StrongMemoize + + def initialize(repository, refs) + @repository, @refs = repository, refs + end + + # Returns the SHA of the first common ancestor + def sha + if unknown_refs.any? + raise UnknownRef, "Can't find merge base for unknown refs: #{unknown_refs.inspect}" + end + + strong_memoize(:sha) do + @repository.merge_base(*commits_for_refs) + end + end + + # Returns the merge base as a Gitlab::Git::Commit + def commit + return unless sha + + @commit ||= @repository.commit_by(oid: sha) + end + + # Returns the refs passed on initialization that aren't found in + # the repository, and thus cannot be used to find a merge base. + def unknown_refs + @unknown_refs ||= Hash[@refs.zip(commits_for_refs)] + .select { |ref, commit| commit.nil? }.keys + end + + private + + def commits_for_refs + @commits_for_refs ||= @repository.commits_by(oids: @refs) + end + end + end +end diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb index 3e11355435b..9521a2d63a0 100644 --- a/lib/gitlab/git/repository.rb +++ b/lib/gitlab/git/repository.rb @@ -366,18 +366,9 @@ module Gitlab end end - # Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/1233 def new_commits(newrev) - gitaly_migrate(:new_commits) do |is_enabled| - if is_enabled - gitaly_ref_client.list_new_commits(newrev) - else - refs = Gitlab::GitalyClient::StorageSettings.allow_disk_access do - rev_list(including: newrev, excluding: :all).split("\n").map(&:strip) - end - - Gitlab::Git::Commit.batch_by_oid(self, refs) - end + wrapped_gitaly_errors do + gitaly_ref_client.list_new_commits(newrev) end end @@ -552,14 +543,8 @@ module Gitlab end def update_branch(branch_name, user:, newrev:, oldrev:) - gitaly_migrate(:operation_user_update_branch) do |is_enabled| - if is_enabled - gitaly_operation_client.user_update_branch(branch_name, user, newrev, oldrev) - else - Gitlab::GitalyClient::StorageSettings.allow_disk_access do - OperationService.new(user, self).update_branch(branch_name, newrev, oldrev) - end - end + wrapped_gitaly_errors do + gitaly_operation_client.user_update_branch(branch_name, user, newrev, oldrev) end end diff --git a/lib/gitlab/git/repository_mirroring.rb b/lib/gitlab/git/repository_mirroring.rb index 65eb5cc18cf..752a91fbb60 100644 --- a/lib/gitlab/git/repository_mirroring.rb +++ b/lib/gitlab/git/repository_mirroring.rb @@ -2,34 +2,7 @@ module Gitlab module Git module RepositoryMirroring def remote_branches(remote_name) - gitaly_migrate(:ref_find_all_remote_branches) do |is_enabled| - if is_enabled - gitaly_ref_client.remote_branches(remote_name) - else - Gitlab::GitalyClient::StorageSettings.allow_disk_access do - rugged_remote_branches(remote_name) - end - end - end - end - - private - - def rugged_remote_branches(remote_name) - branches = [] - - rugged.references.each("refs/remotes/#{remote_name}/*").map do |ref| - name = ref.name.sub(%r{\Arefs/remotes/#{remote_name}/}, '') - - begin - target_commit = Gitlab::Git::Commit.find(self, ref.target.oid) - branches << Gitlab::Git::Branch.new(self, name, ref.target, target_commit) - rescue Rugged::ReferenceError - # Omit invalid branch - end - end - - branches + gitaly_ref_client.remote_branches(remote_name) end end end diff --git a/lib/gitlab/gitaly_client/diff.rb b/lib/gitlab/gitaly_client/diff.rb index d98a0ce988f..af9d674535b 100644 --- a/lib/gitlab/gitaly_client/diff.rb +++ b/lib/gitlab/gitaly_client/diff.rb @@ -1,7 +1,7 @@ module Gitlab module GitalyClient class Diff - ATTRS = %i(from_path to_path old_mode new_mode from_id to_id patch overflow_marker collapsed).freeze + ATTRS = %i(from_path to_path old_mode new_mode from_id to_id patch overflow_marker collapsed too_large).freeze include AttributesBag end diff --git a/lib/gitlab/github_import/bulk_importing.rb b/lib/gitlab/github_import/bulk_importing.rb index 147597289cf..da2f96b5c4b 100644 --- a/lib/gitlab/github_import/bulk_importing.rb +++ b/lib/gitlab/github_import/bulk_importing.rb @@ -15,10 +15,12 @@ module Gitlab end # Bulk inserts the given rows into the database. - def bulk_insert(model, rows, batch_size: 100) + def bulk_insert(model, rows, batch_size: 100, pre_hook: nil) rows.each_slice(batch_size) do |slice| + pre_hook.call(slice) if pre_hook Gitlab::Database.bulk_insert(model.table_name, slice) end + rows end end end diff --git a/lib/gitlab/github_import/importer/diff_note_importer.rb b/lib/gitlab/github_import/importer/diff_note_importer.rb index 8274f37d358..d562958e955 100644 --- a/lib/gitlab/github_import/importer/diff_note_importer.rb +++ b/lib/gitlab/github_import/importer/diff_note_importer.rb @@ -13,7 +13,7 @@ module Gitlab @note = note @project = project @client = client - @user_finder = UserFinder.new(project, client) + @user_finder = GithubImport::UserFinder.new(project, client) end def execute diff --git a/lib/gitlab/github_import/importer/issue_importer.rb b/lib/gitlab/github_import/importer/issue_importer.rb index 31fefebf787..cb4d7a6a0b6 100644 --- a/lib/gitlab/github_import/importer/issue_importer.rb +++ b/lib/gitlab/github_import/importer/issue_importer.rb @@ -19,7 +19,7 @@ module Gitlab @issue = issue @project = project @client = client - @user_finder = UserFinder.new(project, client) + @user_finder = GithubImport::UserFinder.new(project, client) @milestone_finder = MilestoneFinder.new(project) @issuable_finder = GithubImport::IssuableFinder.new(project, issue) end @@ -55,7 +55,11 @@ module Gitlab updated_at: issue.updated_at } - GithubImport.insert_and_return_id(attributes, project.issues) + GithubImport.insert_and_return_id(attributes, project.issues).tap do |id| + # We use .insert_and_return_id which effectively disables all callbacks. + # Trigger iid logic here to make sure we track internal id values consistently. + project.issues.find(id).ensure_project_iid! + end rescue ActiveRecord::InvalidForeignKey # It's possible the project has been deleted since scheduling this # job. In this case we'll just skip creating the issue. diff --git a/lib/gitlab/github_import/importer/milestones_importer.rb b/lib/gitlab/github_import/importer/milestones_importer.rb index c53480e828a..94eb9136b9a 100644 --- a/lib/gitlab/github_import/importer/milestones_importer.rb +++ b/lib/gitlab/github_import/importer/milestones_importer.rb @@ -17,10 +17,20 @@ module Gitlab end def execute - bulk_insert(Milestone, build_milestones) + # We insert records in bulk, by-passing any standard model callbacks. + # The pre_hook here makes sure we track internal ids consistently. + # Note this has to be called before performing an insert of a batch + # because we're outside a transaction scope here. + bulk_insert(Milestone, build_milestones, pre_hook: method(:track_greatest_iid)) build_milestones_cache end + def track_greatest_iid(slice) + greatest_iid = slice.max { |e| e[:iid] }[:iid] + + InternalId.track_greatest(nil, { project: project }, :milestones, greatest_iid, ->(_) { project.milestones.maximum(:iid) }) + end + def build_milestones build_database_rows(each_milestone) end diff --git a/lib/gitlab/github_import/importer/note_importer.rb b/lib/gitlab/github_import/importer/note_importer.rb index c890f2df360..2b06d1b3baf 100644 --- a/lib/gitlab/github_import/importer/note_importer.rb +++ b/lib/gitlab/github_import/importer/note_importer.rb @@ -13,7 +13,7 @@ module Gitlab @note = note @project = project @client = client - @user_finder = UserFinder.new(project, client) + @user_finder = GithubImport::UserFinder.new(project, client) end def execute diff --git a/lib/gitlab/github_import/importer/pull_request_importer.rb b/lib/gitlab/github_import/importer/pull_request_importer.rb index 6b3688c4381..ed17aa54373 100644 --- a/lib/gitlab/github_import/importer/pull_request_importer.rb +++ b/lib/gitlab/github_import/importer/pull_request_importer.rb @@ -15,7 +15,7 @@ module Gitlab @pull_request = pull_request @project = project @client = client - @user_finder = UserFinder.new(project, client) + @user_finder = GithubImport::UserFinder.new(project, client) @milestone_finder = MilestoneFinder.new(project) @issuable_finder = GithubImport::IssuableFinder.new(project, pull_request) @@ -76,7 +76,13 @@ module Gitlab merge_request_id = GithubImport .insert_and_return_id(attributes, project.merge_requests) - [project.merge_requests.find(merge_request_id), false] + merge_request = project.merge_requests.find(merge_request_id) + + # We use .insert_and_return_id which effectively disables all callbacks. + # Trigger iid logic here to make sure we track internal id values consistently. + merge_request.ensure_target_project_iid! + + [merge_request, false] end rescue ActiveRecord::InvalidForeignKey # It's possible the project has been deleted since scheduling this diff --git a/lib/gitlab/i18n.rb b/lib/gitlab/i18n.rb index 343487bc361..b8213929c6a 100644 --- a/lib/gitlab/i18n.rb +++ b/lib/gitlab/i18n.rb @@ -22,7 +22,8 @@ module Gitlab 'tr_TR' => 'Türkçe', 'id_ID' => 'Bahasa Indonesia', 'fil_PH' => 'Filipino', - 'pl_PL' => 'Polski' + 'pl_PL' => 'Polski', + 'cs_CZ' => 'Čeština' }.freeze def available_locales diff --git a/lib/gitlab/import_export/members_mapper.rb b/lib/gitlab/import_export/members_mapper.rb index ac827cbe1ca..bcbaf00e11b 100644 --- a/lib/gitlab/import_export/members_mapper.rb +++ b/lib/gitlab/import_export/members_mapper.rb @@ -45,7 +45,7 @@ module Gitlab end def ensure_default_member! - @project.project_members.destroy_all + @project.project_members.destroy_all # rubocop: disable DestroyAll ProjectMember.create!(user: @user, access_level: ProjectMember::MAINTAINER, source_id: @project.id, importing: true) end diff --git a/lib/gitlab/import_export/relation_factory.rb b/lib/gitlab/import_export/relation_factory.rb index 091e81028bb..81807ed659c 100644 --- a/lib/gitlab/import_export/relation_factory.rb +++ b/lib/gitlab/import_export/relation_factory.rb @@ -47,7 +47,7 @@ module Gitlab end def initialize(relation_sym:, relation_hash:, members_mapper:, user:, project:, excluded_keys: []) - @relation_name = OVERRIDES[relation_sym] || relation_sym + @relation_name = self.class.overrides[relation_sym] || relation_sym @relation_hash = relation_hash.except('noteable_id') @members_mapper = members_mapper @user = user @@ -76,6 +76,10 @@ module Gitlab generate_imported_object end + def self.overrides + OVERRIDES + end + private def setup_models diff --git a/lib/gitlab/setup_helper.rb b/lib/gitlab/setup_helper.rb index b2d75aac1d0..5b68e4470cd 100644 --- a/lib/gitlab/setup_helper.rb +++ b/lib/gitlab/setup_helper.rb @@ -1,3 +1,5 @@ +require 'toml-rb' + module Gitlab module SetupHelper class << self @@ -9,7 +11,7 @@ module Gitlab # because it uses a Unix socket. # For development and testing purposes, an extra storage is added to gitaly, # which is not known to Rails, but must be explicitly stubbed. - def gitaly_configuration_toml(gitaly_dir, gitaly_ruby: true) + def gitaly_configuration_toml(gitaly_dir, storage_paths, gitaly_ruby: true) storages = [] address = nil @@ -24,10 +26,7 @@ module Gitlab address = val['gitaly_address'] end - # https://gitlab.com/gitlab-org/gitaly/issues/1238 - Gitlab::GitalyClient::StorageSettings.allow_disk_access do - storages << { name: key, path: val.legacy_disk_path } - end + storages << { name: key, path: storage_paths[key] } end if Rails.env.test? @@ -44,12 +43,12 @@ module Gitlab end # rubocop:disable Rails/Output - def create_gitaly_configuration(dir, force: false) + def create_gitaly_configuration(dir, storage_paths, force: false) config_path = File.join(dir, 'config.toml') FileUtils.rm_f(config_path) if force File.open(config_path, File::WRONLY | File::CREAT | File::EXCL) do |f| - f.puts gitaly_configuration_toml(dir) + f.puts gitaly_configuration_toml(dir, storage_paths) end rescue Errno::EEXIST puts "Skipping config.toml generation:" diff --git a/lib/gitlab/template/finders/base_template_finder.rb b/lib/gitlab/template/finders/base_template_finder.rb index 473b05257c6..a5105439b12 100644 --- a/lib/gitlab/template/finders/base_template_finder.rb +++ b/lib/gitlab/template/finders/base_template_finder.rb @@ -21,7 +21,7 @@ module Gitlab def category_directory(category) return @base_dir unless category.present? - @base_dir + @categories[category] + File.join(@base_dir, @categories[category]) end class << self diff --git a/lib/gitlab/template/finders/repo_template_finder.rb b/lib/gitlab/template/finders/repo_template_finder.rb index 33f07fa0120..29bc2393ff9 100644 --- a/lib/gitlab/template/finders/repo_template_finder.rb +++ b/lib/gitlab/template/finders/repo_template_finder.rb @@ -27,7 +27,7 @@ module Gitlab directory = select_directory(file_name) raise FileNotFoundError if directory.nil? - category_directory(directory) + file_name + File.join(category_directory(directory), file_name) end def list_files_for(dir) @@ -37,8 +37,8 @@ module Gitlab entries = @repository.tree(:head, dir).entries - names = entries.map(&:name) - names.select { |f| f =~ self.class.filter_regex(@extension) } + paths = entries.map(&:path) + paths.select { |f| f =~ self.class.filter_regex(@extension) } end private @@ -47,10 +47,10 @@ module Gitlab return [] unless @commit # Insert root as directory - directories = ["", @categories.keys] + directories = ["", *@categories.keys] directories.find do |category| - path = category_directory(category) + file_name + path = File.join(category_directory(category), file_name) @repository.blob_at(@commit.id, path) end end diff --git a/lib/tasks/gitlab/gitaly.rake b/lib/tasks/gitlab/gitaly.rake index e9ca6404fe8..80de3d2ef51 100644 --- a/lib/tasks/gitlab/gitaly.rake +++ b/lib/tasks/gitlab/gitaly.rake @@ -1,13 +1,12 @@ namespace :gitlab do namespace :gitaly do desc "GitLab | Install or upgrade gitaly" - task :install, [:dir, :repo] => :gitlab_environment do |t, args| - require 'toml-rb' - + task :install, [:dir, :storage_path, :repo] => :gitlab_environment do |t, args| warn_user_is_not_gitlab - unless args.dir.present? - abort %(Please specify the directory where you want to install gitaly:\n rake "gitlab:gitaly:install[/home/git/gitaly]") + unless args.dir.present? && args.storage_path.present? + abort %(Please specify the directory where you want to install gitaly and the path for the default storage +Usage: rake "gitlab:gitaly:install[/installation/dir,/storage/path]") end args.with_defaults(repo: 'https://gitlab.com/gitlab-org/gitaly.git') @@ -27,7 +26,8 @@ namespace :gitlab do "BUNDLE_PATH=#{Bundler.bundle_path}") end - Gitlab::SetupHelper.create_gitaly_configuration(args.dir) + storage_paths = { 'default' => args.storage_path } + Gitlab::SetupHelper.create_gitaly_configuration(args.dir, storage_paths) Dir.chdir(args.dir) do # In CI we run scripts/gitaly-test-build instead of this command unless ENV['CI'].present? @@ -35,17 +35,5 @@ namespace :gitlab do end end end - - desc "GitLab | Print storage configuration in TOML format" - task storage_config: :environment do - require 'toml-rb' - - puts "# Gitaly storage configuration generated from #{Gitlab.config.source} on #{Time.current.to_s(:long)}" - puts "# This is in TOML format suitable for use in Gitaly's config.toml file." - - # Exclude gitaly-ruby configuration because that depends on the gitaly - # installation directory. - puts Gitlab::SetupHelper.gitaly_configuration_toml('', gitaly_ruby: false) - end end end diff --git a/lib/tasks/gitlab/site_statistics.rake b/lib/tasks/gitlab/site_statistics.rake new file mode 100644 index 00000000000..7d24ec72a9d --- /dev/null +++ b/lib/tasks/gitlab/site_statistics.rake @@ -0,0 +1,23 @@ +namespace :gitlab do + desc "GitLab | Refresh Site Statistics counters" + task refresh_site_statistics: :environment do + puts 'Updating Site Statistics counters: ' + + print '* Repositories... ' + SiteStatistic.transaction do + # see https://gitlab.com/gitlab-org/gitlab-ce/issues/48967 + ActiveRecord::Base.connection.execute('SET LOCAL statement_timeout TO 0') if Gitlab::Database.postgresql? + SiteStatistic.update_all('repositories_count = (SELECT COUNT(*) FROM projects)') + end + puts 'OK!'.color(:green) + + print '* Wikis... ' + SiteStatistic.transaction do + # see https://gitlab.com/gitlab-org/gitlab-ce/issues/48967 + ActiveRecord::Base.connection.execute('SET LOCAL statement_timeout TO 0') if Gitlab::Database.postgresql? + SiteStatistic.update_all('wikis_count = (SELECT COUNT(*) FROM project_features WHERE wiki_access_level != 0)') + end + puts 'OK!'.color(:green) + puts + end +end diff --git a/lib/tasks/gitlab/traces.rake b/lib/tasks/gitlab/traces.rake index ddcca69711f..5a232091a7e 100644 --- a/lib/tasks/gitlab/traces.rake +++ b/lib/tasks/gitlab/traces.rake @@ -18,5 +18,22 @@ namespace :gitlab do logger.info("Scheduled #{job_ids.count} jobs. From #{job_ids.min} to #{job_ids.max}") end end + + task migrate: :environment do + logger = Logger.new(STDOUT) + logger.info('Starting transfer of job traces') + + Ci::Build.joins(:project) + .with_archived_trace_stored_locally + .find_each(batch_size: 10) do |build| + begin + build.job_artifacts_trace.file.migrate!(ObjectStorage::Store::REMOTE) + + logger.info("Transferred job trace of #{build.id} to object storage") + rescue => e + logger.error("Failed to transfer artifacts of #{build.id} with error: #{e.message}") + end + end + end end end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 29593e4c965..d8addc0a718 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -101,6 +101,9 @@ msgstr[1] "" msgid "%{filePath} deleted" msgstr "" +msgid "%{firstLabel} +%{labelCount} more" +msgstr "" + msgid "%{group_docs_link_start}Groups%{group_docs_link_end} allow you to manage and collaborate across multiple projects. Members of a group have access to all of its projects." msgstr "" @@ -232,6 +235,9 @@ msgstr "" msgid "<code>\"johnsmith@example.com\": \"johnsmith@example.com\"</code> will add \"By <a href=\"#\">johnsmith@example.com</a>\" to all issues and comments originally created by johnsmith@example.com. By default, the email address or username is masked to ensure the user's privacy. Use this option if you want to show the full email address." msgstr "" +msgid "<strong>%{changedFilesLength} unstaged</strong> and <strong>%{stagedFilesLength} staged</strong> changes" +msgstr "" + msgid "<strong>%{group_name}</strong> group members" msgstr "" @@ -343,6 +349,12 @@ msgstr "" msgid "Admin area" msgstr "" +msgid "AdminArea| You are about to permanently delete the user %{username}. Issues, merge requests, and groups linked to them will be transferred to a system-wide \"Ghost-user\". To avoid data loss, consider using the %{strong_start}block user%{strong_end} feature instead. Once you %{strong_start}Delete user%{strong_end}, it cannot be undone or recovered." +msgstr "" + +msgid "AdminArea| You are about to permanently delete the user %{username}. This will delete all of the issues, merge requests, and groups linked to them. To avoid data loss, consider using the %{strong_start}block user%{strong_end} feature instead. Once you %{strong_start}Delete user%{strong_end}, it cannot be undone or recovered." +msgstr "" + msgid "AdminArea|Stop all jobs" msgstr "" @@ -361,6 +373,9 @@ msgstr "" msgid "AdminHealthPageLink|health page" msgstr "" +msgid "AdminProjects| You’re about to permanently delete the project %{projectName}, its repository, and all related resources including issues, merge requests, etc.. Once you confirm and press %{strong_start}Delete project%{strong_end}, it cannot be undone or recovered." +msgstr "" + msgid "AdminProjects|Delete" msgstr "" @@ -496,7 +511,7 @@ msgstr "" msgid "An error occurred while getting projects" msgstr "" -msgid "An error occurred while importing project: ${details}" +msgid "An error occurred while importing project: %{details}" msgstr "" msgid "An error occurred while loading commit signatures" @@ -640,6 +655,9 @@ msgstr "" msgid "Authentication log" msgstr "" +msgid "Authentication method" +msgstr "" + msgid "Author" msgstr "" @@ -799,6 +817,9 @@ msgstr "" msgid "Badges|This project has no badges" msgstr "" +msgid "Badges|You are going to delete this badge. Deleted badges <strong>cannot</strong> be restored." +msgstr "" + msgid "Badges|Your badges" msgstr "" @@ -1242,6 +1263,9 @@ msgstr "" msgid "ClusterIntegration|%{appList} was successfully installed on your Kubernetes cluster" msgstr "" +msgid "ClusterIntegration|%{boldNotice} This will add some extra resources like a load balancer, which may incur additional costs depending on the hosting provider your Kubernetes cluster is installed on. If you are using Google Kubernetes Engine, you can %{pricingLink}." +msgstr "" + msgid "ClusterIntegration|API URL" msgstr "" @@ -1251,12 +1275,21 @@ msgstr "" msgid "ClusterIntegration|Advanced options on this Kubernetes cluster's integration" msgstr "" +msgid "ClusterIntegration|After installing Ingress, you will need to point your wildcard DNS at the generated external IP address in order to view your app after it is deployed. %{ingressHelpLink}" +msgstr "" + msgid "ClusterIntegration|An error occured while trying to fetch project zones: %{error}" msgstr "" msgid "ClusterIntegration|An error occured while trying to fetch your projects: %{error}" msgstr "" +msgid "ClusterIntegration|An error occured while trying to fetch zone machine types: %{error}" +msgstr "" + +msgid "ClusterIntegration|An error occurred when trying to contact the Google Cloud API. Please try again later." +msgstr "" + msgid "ClusterIntegration|Applications" msgstr "" @@ -1323,6 +1356,9 @@ msgstr "" msgid "ClusterIntegration|GitLab Runner" msgstr "" +msgid "ClusterIntegration|GitLab Runner connects to this project's repository and executes CI/CD jobs, pushing results back and deploying, applications to production." +msgstr "" + msgid "ClusterIntegration|Google Cloud Platform project" msgstr "" @@ -1335,6 +1371,9 @@ msgstr "" msgid "ClusterIntegration|Helm Tiller" msgstr "" +msgid "ClusterIntegration|Helm streamlines installing and managing Kubernetes applications. Tiller runs inside of your Kubernetes Cluster, and manages releases of your charts." +msgstr "" + msgid "ClusterIntegration|Hide" msgstr "" @@ -1344,9 +1383,15 @@ msgstr "" msgid "ClusterIntegration|Ingress IP Address" msgstr "" +msgid "ClusterIntegration|Ingress gives you a way to route requests to services based on the request host or path, centralizing a number of services into a single entrypoint." +msgstr "" + msgid "ClusterIntegration|Install" msgstr "" +msgid "ClusterIntegration|Install applications on your Kubernetes cluster. Read more about %{helpLink}" +msgstr "" + msgid "ClusterIntegration|Installed" msgstr "" @@ -1365,6 +1410,9 @@ msgstr "" msgid "ClusterIntegration|JupyterHub" msgstr "" +msgid "ClusterIntegration|JupyterHub, a multi-user Hub, spawns, manages, and proxies multiple instances of the single-user Jupyter notebook server. JupyterHub can be used to serve notebooks to a class of students, a corporate data science group, or a scientific research group." +msgstr "" + msgid "ClusterIntegration|Kubernetes cluster" msgstr "" @@ -1452,6 +1500,9 @@ msgstr "" msgid "ClusterIntegration|Please make sure that your Google account meets the following requirements:" msgstr "" +msgid "ClusterIntegration|Point a wildcard DNS to this generated IP address in order to access your application after it has been deployed." +msgstr "" + msgid "ClusterIntegration|Project namespace" msgstr "" @@ -1461,6 +1512,9 @@ msgstr "" msgid "ClusterIntegration|Prometheus" msgstr "" +msgid "ClusterIntegration|Prometheus is an open-source monitoring system with %{gitlabIntegrationLink} to monitor deployed applications." +msgstr "" + msgid "ClusterIntegration|Read our %{link_to_help_page} on Kubernetes cluster integration." msgstr "" @@ -1473,6 +1527,9 @@ msgstr "" msgid "ClusterIntegration|Remove this Kubernetes cluster's configuration from this project. This will not delete your actual Kubernetes cluster." msgstr "" +msgid "ClusterIntegration|Replace this with your own hostname if you want. If you do so, point hostname to Ingress IP Address from above." +msgstr "" + msgid "ClusterIntegration|Request to begin installing failed" msgstr "" @@ -1527,6 +1584,9 @@ msgstr "" msgid "ClusterIntegration|Something went wrong while installing %{title}" msgstr "" +msgid "ClusterIntegration|The IP address is in the process of being assigned. Please check your Kubernetes cluster or Quotas on Google Kubernetes Engine if it takes a long time." +msgstr "" + msgid "ClusterIntegration|The default cluster configuration grants access to a wide set of functionalities needed to successfully build and deploy a containerised application." msgstr "" @@ -1545,6 +1605,9 @@ msgstr "" msgid "ClusterIntegration|Validating project billing status" msgstr "" +msgid "ClusterIntegration|We could not verify that one of your projects on GCP has billing enabled. Please try again." +msgstr "" + msgid "ClusterIntegration|With a Kubernetes cluster associated to this project, you can use review apps, deploy your applications, run your pipelines, and much more in an easy way." msgstr "" @@ -1963,6 +2026,9 @@ msgstr "" msgid "Cycle Analytics" msgstr "" +msgid "Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project." +msgstr "" + msgid "CycleAnalyticsStage|Code" msgstr "" @@ -1993,6 +2059,9 @@ msgstr "" msgid "DashboardProjects|Personal" msgstr "" +msgid "Debug" +msgstr "" + msgid "Dec" msgstr "" @@ -2175,6 +2244,9 @@ msgstr "" msgid "Diffs|Something went wrong while fetching diff lines." msgstr "" +msgid "Direction" +msgstr "" + msgid "Directory name" msgstr "" @@ -2355,6 +2427,9 @@ msgstr "" msgid "Environments|Environments" msgstr "" +msgid "Environments|Environments are places where code gets deployed, such as staging or production." +msgstr "" + msgid "Environments|Job" msgstr "" @@ -2367,6 +2442,9 @@ msgstr "" msgid "Environments|No deployments yet" msgstr "" +msgid "Environments|Note that this action will stop the environment, but it will %{emphasisStart}not%{emphasisEnd} have an effect on any existing deployment due to no “stop environment action” being defined in the %{ciConfigLinkStart}.gitlab-ci.yml%{ciConfigLinkEnd} file." +msgstr "" + msgid "Environments|Note that this action will stop the environment, but it will %{emphasis_start}not%{emphasis_end} have an effect on any existing deployment due to no “stop environment action” being defined in the %{ci_config_link_start}.gitlab-ci.yml%{ci_config_link_end} file." msgstr "" @@ -2397,6 +2475,9 @@ msgstr "" msgid "Environments|You don't have any environments right now." msgstr "" +msgid "Error" +msgstr "" + msgid "Error Reporting and Logging" msgstr "" @@ -2445,6 +2526,9 @@ msgstr "" msgid "Error updating todo status." msgstr "" +msgid "Error while loading the merge request. Please try again." +msgstr "" + msgid "Estimated" msgstr "" @@ -2523,6 +2607,9 @@ msgstr "" msgid "Failed to remove issue from board, please try again." msgstr "" +msgid "Failed to remove mirror." +msgstr "" + msgid "Failed to remove the pipeline schedule" msgstr "" @@ -2718,6 +2805,9 @@ msgstr "" msgid "Go back" msgstr "" +msgid "Go to" +msgstr "" + msgid "Go to %{link_to_google_takeout}." msgstr "" @@ -2942,6 +3032,9 @@ msgstr "" msgid "IDE|Review" msgstr "" +msgid "IP Address" +msgstr "" + msgid "Identifier" msgstr "" @@ -3026,12 +3119,21 @@ msgstr "" msgid "Include a Terms of Service agreement and Privacy Policy that all users must accept." msgstr "" +msgid "Include the username in the URL if required: <code>https://username@gitlab.company.com/group/project.git</code>." +msgstr "" + msgid "Incompatible Project" msgstr "" +msgid "Indicates whether this runner can pick jobs without tags" +msgstr "" + msgid "Inline" msgstr "" +msgid "Input your repository URL" +msgstr "" + msgid "Install GitLab Runner" msgstr "" @@ -3098,6 +3200,51 @@ msgstr "" msgid "Jobs" msgstr "" +msgid "Job|Are you sure you want to erase this job?" +msgstr "" + +msgid "Job|Browse" +msgstr "" + +msgid "Job|Complete Raw" +msgstr "" + +msgid "Job|Download" +msgstr "" + +msgid "Job|Erase job log" +msgstr "" + +msgid "Job|Job artifacts" +msgstr "" + +msgid "Job|Job has been erased" +msgstr "" + +msgid "Job|Job has been erased by" +msgstr "" + +msgid "Job|Keep" +msgstr "" + +msgid "Job|Scroll to bottom" +msgstr "" + +msgid "Job|Scroll to top" +msgstr "" + +msgid "Job|Show complete raw" +msgstr "" + +msgid "Job|The artifacts were removed" +msgstr "" + +msgid "Job|The artifacts will be removed" +msgstr "" + +msgid "Job|This job is stuck, because the project doesn't have any runners online assigned to it." +msgstr "" + msgid "Jul" msgstr "" @@ -3179,6 +3326,9 @@ msgstr "" msgid "Labels|Promote Label" msgstr "" +msgid "Labels|Promoting %{labelTitle} will make it available for all projects inside %{groupName}. Existing project labels with the same title will be merged. This action cannot be reversed." +msgstr "" + msgid "Last %d day" msgid_plural "Last %d days" msgstr[0] "" @@ -3238,6 +3388,11 @@ msgstr "" msgid "Leave the \"File type\" and \"Delivery method\" options on their default values." msgstr "" +msgid "Limited to showing %d event at most" +msgid_plural "Limited to showing %d events at most" +msgstr[0] "" +msgstr[1] "" + msgid "LinkedIn" msgstr "" @@ -3274,6 +3429,9 @@ msgstr "" msgid "Lock not found" msgstr "" +msgid "Lock this %{issuableDisplayName}? Only <strong>project members</strong> will be able to comment." +msgstr "" + msgid "Lock to current projects" msgstr "" @@ -3349,6 +3507,9 @@ msgstr "" msgid "Maximum git storage failures" msgstr "" +msgid "Maximum job timeout" +msgstr "" + msgid "May" msgstr "" @@ -3397,6 +3558,9 @@ msgstr "" msgid "MergeRequests|View replaced file @ %{commitId}" msgstr "" +msgid "MergeRequest| %{paragraphStart}changed the description %{descriptionChangedTimes} times %{timeDifferenceMinutes}%{paragraphEnd}" +msgstr "" + msgid "Merged" msgstr "" @@ -3445,6 +3609,12 @@ msgstr "" msgid "Milestones" msgstr "" +msgid "Milestones| You’re about to permanently delete the milestone %{milestoneTitle} from this project and remove it from %{issuesWithCount} and %{mergeRequestsWithCount}. Once deleted, it cannot be undone or recovered." +msgstr "" + +msgid "Milestones| You’re about to permanently delete the milestone %{milestoneTitle} from this project. %{milestoneTitle} is not currently used in any issues or merge requests." +msgstr "" + msgid "Milestones|Delete milestone" msgstr "" @@ -3463,6 +3633,24 @@ msgstr "" msgid "Milestones|Promote Milestone" msgstr "" +msgid "Milestones|Promoting %{milestoneTitle} will make it available for all projects inside %{groupName}. Existing project milestones with the same title will be merged. This action cannot be reversed." +msgstr "" + +msgid "Mirror a repository" +msgstr "" + +msgid "Mirror direction" +msgstr "" + +msgid "Mirror repository" +msgstr "" + +msgid "Mirrored repositories" +msgstr "" + +msgid "Mirroring repositories" +msgstr "" + msgid "MissingSSHKeyWarningLink|add an SSH key" msgstr "" @@ -3523,6 +3711,9 @@ msgstr "" msgid "Network" msgstr "" +msgid "Never" +msgstr "" + msgid "New" msgstr "" @@ -3615,6 +3806,9 @@ msgstr "" msgid "No connection could be made to a Gitaly Server, please check your logs!" msgstr "" +msgid "No container images stored for this project. Add one by following the instructions above." +msgstr "" + msgid "No due date" msgstr "" @@ -3663,6 +3857,12 @@ msgstr "" msgid "None" msgstr "" +msgid "Not all comments are displayed because you're comparing two versions of the diff." +msgstr "" + +msgid "Not all comments are displayed because you're viewing an old version of the diff." +msgstr "" + msgid "Not allowed to merge" msgstr "" @@ -3774,6 +3974,11 @@ msgstr "" msgid "OfSearchInADropdown|Filter" msgstr "" +msgid "One more item" +msgid_plural "%d more items" +msgstr[0] "" +msgstr[1] "" + msgid "One or more of your Bitbucket projects cannot be imported into GitLab directly because they use Subversion or Mercurial for version control, rather than Git." msgstr "" @@ -3789,12 +3994,18 @@ msgstr "" msgid "Only comments from the following commit are shown below" msgstr "" +msgid "Only mirror protected branches" +msgstr "" + msgid "Only project members can comment." msgstr "" msgid "Oops, are you sure?" msgstr "" +msgid "Open" +msgstr "" + msgid "Open in Xcode" msgstr "" @@ -3870,9 +4081,15 @@ msgstr "" msgid "Pause" msgstr "" +msgid "Paused Runners don't accept new jobs" +msgstr "" + msgid "Pending" msgstr "" +msgid "People without permission will never get a notification and won't be able to comment." +msgstr "" + msgid "Per job. If a job passes this threshold, it will be marked as failed" msgstr "" @@ -3891,6 +4108,9 @@ msgstr "" msgid "Pipeline" msgstr "" +msgid "Pipeline %{pipelineLinkStart} #%{pipelineId} %{pipelineLinkEnd} from %{pipelineLinkRefStart} %{pipelineRef} %{pipelineLinkRefEnd}" +msgstr "" + msgid "Pipeline Health" msgstr "" @@ -3975,6 +4195,9 @@ msgstr "" msgid "Pipelines|Clear Runner Caches" msgstr "" +msgid "Pipelines|Continuous Integration can help catch bugs by running your tests automatically, while Continuous Deployment can help you deliver code to your product environment." +msgstr "" + msgid "Pipelines|Get started with Pipelines" msgstr "" @@ -3996,6 +4219,9 @@ msgstr "" msgid "Pipelines|There are currently no pipelines." msgstr "" +msgid "Pipelines|There was an error fetching the pipelines. Try again in a few moments or contact your support team." +msgstr "" + msgid "Pipelines|This project is not currently set up to run pipelines." msgstr "" @@ -4110,6 +4336,12 @@ msgstr "" msgid "Profile Settings" msgstr "" +msgid "Profiles| You are about to permanently delete %{yourAccount}, and all of the issues, merge requests, and groups linked to your account. Once you confirm %{deleteAccount}, it cannot be undone or recovered." +msgstr "" + +msgid "Profiles| You are going to change the username %{currentUsernameBold} to %{newUsernameBold}. Profile and projects will be redirected to the %{newUsername} namespace but this redirect will expire once the %{currentUsername} namespace is registered by another user or group. Please update your Git repository remotes as soon as possible." +msgstr "" + msgid "Profiles|Account scheduled for removal." msgstr "" @@ -4305,6 +4537,9 @@ msgstr "" msgid "ProjectsDropdown|Sorry, no projects matched your search" msgstr "" +msgid "ProjectsDropdown|This feature requires browser localStorage support" +msgstr "" + msgid "PrometheusDashboard|Time" msgstr "" @@ -4383,6 +4618,9 @@ msgstr "" msgid "Promote to group label" msgstr "" +msgid "Protected" +msgstr "" + msgid "Protip:" msgstr "" @@ -4398,6 +4636,9 @@ msgstr "" msgid "Public pipelines" msgstr "" +msgid "Push" +msgstr "" + msgid "Push events" msgstr "" @@ -4425,6 +4666,11 @@ msgstr "" msgid "Reference:" msgstr "" +msgid "Refreshing in a second to show the updated status..." +msgid_plural "Refreshing in %d seconds to show the updated status..." +msgstr[0] "" +msgstr[1] "" + msgid "Register / Sign In" msgstr "" @@ -4572,6 +4818,9 @@ msgstr "" msgid "Retry verification" msgstr "" +msgid "Reveal Variables" +msgstr "" + msgid "Reveal value" msgid_plural "Reveal values" msgstr[0] "" @@ -4595,6 +4844,9 @@ msgstr "" msgid "Revoke" msgstr "" +msgid "Run untagged jobs" +msgstr "" + msgid "Runner token" msgstr "" @@ -4607,6 +4859,9 @@ msgstr "" msgid "Runners can be placed on separate users, servers, and even on your local machine." msgstr "" +msgid "Runners page" +msgstr "" + msgid "Running" msgstr "" @@ -4799,6 +5054,9 @@ msgstr "" msgid "Set up Koding" msgstr "" +msgid "Set up your project to automatically push and/or pull changes to/from another repository. Branches, tags, and commits will be synced automatically." +msgstr "" + msgid "SetPasswordToCloneLink|set a password" msgstr "" @@ -4879,6 +5137,12 @@ msgstr "" msgid "Something went wrong on our end. Please try again!" msgstr "" +msgid "Something went wrong trying to change the confidentiality of this issue" +msgstr "" + +msgid "Something went wrong trying to change the locked state of this %{issuableDisplayName}" +msgstr "" + msgid "Something went wrong when toggling the button" msgstr "" @@ -5208,12 +5472,18 @@ msgstr "" msgid "Test coverage parsing" msgstr "" +msgid "The Git LFS objects will <strong>not</strong> be synced." +msgstr "" + msgid "The Issue Tracker is the place to add things that need to be improved or solved in a project" msgstr "" msgid "The Issue Tracker is the place to add things that need to be improved or solved in a project. You can register or sign in to create issues for this project." msgstr "" +msgid "The character highlighter helps you keep the subject line to %{titleLength} characters and wrap the body at %{bodyLength} so they are readable in git." +msgstr "" + msgid "The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request." msgstr "" @@ -5244,6 +5514,9 @@ msgstr "" msgid "The phase of the development lifecycle." msgstr "" +msgid "The pipelines schedule runs pipelines in the future, repeatedly, for specific branches or tags. Those scheduled pipelines will inherit limited project access based on their associated user." +msgstr "" + msgid "The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit." msgstr "" @@ -5265,6 +5538,9 @@ msgstr "" msgid "The repository must be accessible over <code>http://</code>, <code>https://</code> or <code>git://</code>." msgstr "" +msgid "The repository must be accessible over <code>http://</code>, <code>https://</code>, <code>ssh://</code> and <code>git://</code>." +msgstr "" + msgid "The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request." msgstr "" @@ -5343,6 +5619,9 @@ msgstr "" msgid "This application will be able to:" msgstr "" +msgid "This branch has changed since you started editing. Would you like to create a new branch?" +msgstr "" + msgid "This diff is collapsed." msgstr "" @@ -5394,6 +5673,12 @@ msgstr "" msgid "This job is in pending state and is waiting to be picked by a runner" msgstr "" +msgid "This job is stuck, because you don't have any active runners online with any of these tags assigned to them:" +msgstr "" + +msgid "This job is stuck, because you don't have any active runners that can run this job." +msgstr "" + msgid "This job requires a manual action" msgstr "" @@ -5403,6 +5688,9 @@ msgstr "" msgid "This merge request is locked." msgstr "" +msgid "This option is disabled as you don't have write permissions for the current branch" +msgstr "" + msgid "This option is disabled while you still have unstaged changes" msgstr "" @@ -5418,15 +5706,27 @@ msgstr "" msgid "This project does not belong to a group and can therefore not make use of group Runners." msgstr "" +msgid "This project does not have billing enabled. To create a cluster, <a href=%{linkToBilling} target=\"_blank\" rel=\"noopener noreferrer\">enable billing <i class=\"fa fa-external-link\" aria-hidden=\"true\"></i></a> and try again." +msgstr "" + msgid "This repository" msgstr "" +msgid "This runner will only run on pipelines triggered on protected branches" +msgstr "" + msgid "This source diff could not be displayed because it is too large." msgstr "" +msgid "This timeout will take precedence when lower than Project-defined timeout" +msgstr "" + msgid "This user has no identities" msgstr "" +msgid "This user will be the author of all events in the activity feed that are the result of an update, like new branches being created or new commits being pushed to existing branches." +msgstr "" + msgid "Time before an issue gets scheduled" msgstr "" @@ -5662,6 +5962,9 @@ msgstr "" msgid "ToggleButton|Toggle Status: ON" msgstr "" +msgid "Token" +msgstr "" + msgid "Too many changes to show." msgstr "" @@ -5680,6 +5983,9 @@ msgstr "" msgid "Trending" msgstr "" +msgid "Trigger" +msgstr "" + msgid "Trigger this manual action" msgstr "" @@ -5698,6 +6004,9 @@ msgstr "" msgid "Unlock" msgstr "" +msgid "Unlock this %{issuableDisplayName}? <strong>Everyone</strong> will be able to comment." +msgstr "" + msgid "Unlocked" msgstr "" @@ -5740,9 +6049,15 @@ msgstr "" msgid "Update" msgstr "" +msgid "Update now" +msgstr "" + msgid "Update your group name, description, avatar, and other general settings." msgstr "" +msgid "Updating" +msgstr "" + msgid "Upload <code>GoogleCodeProjectHosting.json</code> here:" msgstr "" @@ -6082,6 +6397,9 @@ msgstr "" msgid "You can resolve the merge conflict using either the Interactive mode, by choosing %{use_ours} or %{use_theirs} buttons, or by editing the files directly. Commit these changes into %{branch_name}" msgstr "" +msgid "You can setup jobs to only use Runners with specific tags. Separate tags with commas." +msgstr "" + msgid "You cannot write to this read-only GitLab instance." msgstr "" @@ -6199,6 +6517,12 @@ msgstr "" msgid "command line instructions" 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 "" + +msgid "confidentiality|You are going to turn on the confidentiality. This means that only team members with <strong>at least Reporter access</strong> are able to see and leave comments on the issue." +msgstr "" + msgid "connecting" msgstr "" @@ -6302,6 +6626,9 @@ msgstr "" msgid "mrWidget|Failed to load deployment statistics" msgstr "" +msgid "mrWidget|Fast-forward merge is not possible. To merge this request, first rebase locally." +msgstr "" + msgid "mrWidget|If the %{branch} branch exists in your local repository, you can merge this merge request manually using the" msgstr "" @@ -6329,9 +6656,15 @@ msgstr "" msgid "mrWidget|Open in Web IDE" msgstr "" +msgid "mrWidget|Pipeline blocked. The pipeline for this merge request requires a manual action to proceed" +msgstr "" + msgid "mrWidget|Plain diff" msgstr "" +msgid "mrWidget|Ready to be merged automatically. Ask someone with write access to this repository to merge this request" +msgstr "" + msgid "mrWidget|Refresh" msgstr "" @@ -6353,6 +6686,9 @@ msgstr "" msgid "mrWidget|Resolve conflicts" msgstr "" +msgid "mrWidget|Resolve these conflicts or ask someone with write access to this repository to merge it locally" +msgstr "" + msgid "mrWidget|Revert" msgstr "" @@ -6371,6 +6707,12 @@ msgstr "" msgid "mrWidget|The changes will be merged into" msgstr "" +msgid "mrWidget|The pipeline for this merge request failed. Please retry the job or push a new commit to fix the failure" +msgstr "" + +msgid "mrWidget|The source branch HEAD has recently changed. Please reload the page and review the changes before merging" +msgstr "" + msgid "mrWidget|The source branch has been removed" msgstr "" diff --git a/locale/unfound_translations.rb b/locale/unfound_translations.rb new file mode 100644 index 00000000000..0826d64049b --- /dev/null +++ b/locale/unfound_translations.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +# Dynamic translations which needs to be marked by `N_` so they can be found by `rake gettext:find`, see: +# https://github.com/grosser/gettext_i18n_rails#unfound-translations-with-rake-gettextfind + +# NotificationSetting.email_events +N_('NotificationEvent|New note') +N_('NotificationEvent|New issue') +N_('NotificationEvent|Reopen issue') +N_('NotificationEvent|Close issue') +N_('NotificationEvent|Reassign issue') +N_('NotificationEvent|New merge request') +N_('NotificationEvent|Close merge request') +N_('NotificationEvent|Reassign merge request') +N_('NotificationEvent|Merge merge request') +N_('NotificationEvent|Failed pipeline') diff --git a/package.json b/package.json index 975dd2619d7..f5fc759f76e 100644 --- a/package.json +++ b/package.json @@ -126,6 +126,8 @@ "eslint-plugin-jasmine": "^2.1.0", "eslint-plugin-promise": "^3.8.0", "eslint-plugin-vue": "^4.5.0", + "gettext-extractor": "^3.3.2", + "gettext-extractor-vue": "^4.0.1", "ignore": "^3.3.7", "istanbul": "^0.4.5", "jasmine-core": "^2.9.0", diff --git a/qa/README.md b/qa/README.md index be4cf89ebbc..f8a5c00efd4 100644 --- a/qa/README.md +++ b/qa/README.md @@ -35,7 +35,7 @@ following call would login to a local [GDK] instance and run all specs in `qa/specs/features`: ``` -bin/qa Test::Instance http://localhost:3000 +bin/qa Test::Instance::All http://localhost:3000 ``` ### Writing tests @@ -48,14 +48,14 @@ You can also supply specific tests to run as another parameter. For example, to run the repository-related specs, you can execute: ``` -bin/qa Test::Instance http://localhost qa/specs/features/repository/ +bin/qa Test::Instance::All http://localhost qa/specs/features/repository/ ``` Since the arguments would be passed to `rspec`, you could use all `rspec` options there. For example, passing `--backtrace` and also line number: ``` -bin/qa Test::Instance http://localhost qa/specs/features/project/create_spec.rb:3 --backtrace +bin/qa Test::Instance::All http://localhost qa/specs/features/project/create_spec.rb:3 --backtrace ``` ### Overriding the authenticated user @@ -67,7 +67,7 @@ If you need to authenticate as a different user, you can provide the `GITLAB_USERNAME` and `GITLAB_PASSWORD` environment variables: ``` -GITLAB_USERNAME=jsmith GITLAB_PASSWORD=password bin/qa Test::Instance https://gitlab.example.com +GITLAB_USERNAME=jsmith GITLAB_PASSWORD=password bin/qa Test::Instance::All https://gitlab.example.com ``` If your user doesn't have permission to default sandbox group @@ -75,13 +75,13 @@ If your user doesn't have permission to default sandbox group `GITLAB_SANDBOX_NAME`: ``` -GITLAB_USERNAME=jsmith GITLAB_PASSWORD=password GITLAB_SANDBOX_NAME=jsmith-qa-sandbox bin/qa Test::Instance https://gitlab.example.com +GITLAB_USERNAME=jsmith GITLAB_PASSWORD=password GITLAB_SANDBOX_NAME=jsmith-qa-sandbox bin/qa Test::Instance::All https://gitlab.example.com ``` In addition, the `GITLAB_USER_TYPE` can be set to "ldap" to sign in as an LDAP user: ``` -GITLAB_USER_TYPE=ldap GITLAB_USERNAME=jsmith GITLAB_PASSWORD=password GITLAB_SANDBOX_NAME=jsmith-qa-sandbox bin/qa Test::Instance https://gitlab.example.com +GITLAB_USER_TYPE=ldap GITLAB_USERNAME=jsmith GITLAB_PASSWORD=password GITLAB_SANDBOX_NAME=jsmith-qa-sandbox bin/qa Test::Instance::All https://gitlab.example.com ``` All [supported environment variables are here](https://gitlab.com/gitlab-org/gitlab-qa#supported-environment-variables). @@ -77,14 +77,16 @@ module QA # autoload :Bootable, 'qa/scenario/bootable' autoload :Actable, 'qa/scenario/actable' - autoload :Taggable, 'qa/scenario/taggable' autoload :Template, 'qa/scenario/template' ## # Test scenario entrypoints. # module Test - autoload :Instance, 'qa/scenario/test/instance' + module Instance + autoload :All, 'qa/scenario/test/instance/all' + autoload :Smoke, 'qa/scenario/test/instance/smoke' + end module Integration autoload :Github, 'qa/scenario/test/integration/github' diff --git a/qa/qa/scenario/taggable.rb b/qa/qa/scenario/taggable.rb deleted file mode 100644 index b1f24d742e0..00000000000 --- a/qa/qa/scenario/taggable.rb +++ /dev/null @@ -1,17 +0,0 @@ -module QA - module Scenario - module Taggable - # rubocop:disable Gitlab/ModuleWithInstanceVariables - - def tags(*tags) - @tags = tags - end - - def focus - @tags.to_a - end - - # rubocop:enable Gitlab/ModuleWithInstanceVariables - end - end -end diff --git a/qa/qa/scenario/template.rb b/qa/qa/scenario/template.rb index d21a9d52997..765b7d317e0 100644 --- a/qa/qa/scenario/template.rb +++ b/qa/qa/scenario/template.rb @@ -1,15 +1,36 @@ module QA module Scenario class Template - def self.perform(*args) - new.tap do |scenario| - yield scenario if block_given? - break scenario.perform(*args) + class << self + def perform(*args) + new.tap do |scenario| + yield scenario if block_given? + break scenario.perform(*args) + end + end + + def tags(*tags) + @tags = tags + end + + def focus + @tags.to_a end end - def perform(*_args) - raise NotImplementedError + def perform(address, *rspec_options) + Runtime::Scenario.define(:gitlab_address, address) + + Specs::Runner.perform do |specs| + specs.tty = true + specs.tags = self.class.focus + specs.options = + if rspec_options.any? + rspec_options + else + File.expand_path('../../specs/features', __dir__) + end + end end end end diff --git a/qa/qa/scenario/test/instance.rb b/qa/qa/scenario/test/instance.rb deleted file mode 100644 index 46eb2dabb11..00000000000 --- a/qa/qa/scenario/test/instance.rb +++ /dev/null @@ -1,36 +0,0 @@ -module QA - module Scenario - module Test - ## - # Base class for running the suite against any GitLab instance, - # including staging and on-premises installation. - # - class Instance < Template - include Bootable - extend Taggable - - tags :core - - def perform(address, *rspec_options) - Runtime::Scenario.define(:gitlab_address, address) - - ## - # Perform before hooks, which are different for CE and EE - # - Runtime::Release.perform_before_hooks - - Specs::Runner.perform do |specs| - specs.tty = true - specs.tags = self.class.focus - specs.options = - if rspec_options.any? - rspec_options - else - ::File.expand_path('../../specs/features', __dir__) - end - end - end - end - end - end -end diff --git a/qa/qa/scenario/test/instance/all.rb b/qa/qa/scenario/test/instance/all.rb new file mode 100644 index 00000000000..a07c26431bd --- /dev/null +++ b/qa/qa/scenario/test/instance/all.rb @@ -0,0 +1,15 @@ +module QA + module Scenario + module Test + ## + # Base class for running the suite against any GitLab instance, + # including staging and on-premises installation. + # + module Instance + class All < Template + include Bootable + end + end + end + end +end diff --git a/qa/qa/scenario/test/instance/smoke.rb b/qa/qa/scenario/test/instance/smoke.rb new file mode 100644 index 00000000000..a7d2cb27f27 --- /dev/null +++ b/qa/qa/scenario/test/instance/smoke.rb @@ -0,0 +1,17 @@ +module QA + module Scenario + module Test + module Instance + ## + # Base class for running the suite against any GitLab instance, + # including staging and on-premises installation. + # + class Smoke < Template + include Bootable + + tags :smoke + end + end + end + end +end diff --git a/qa/qa/specs/features/api/basics_spec.rb b/qa/qa/specs/features/api/basics_spec.rb index 6563b56d1b4..7defb2ea93e 100644 --- a/qa/qa/specs/features/api/basics_spec.rb +++ b/qa/qa/specs/features/api/basics_spec.rb @@ -1,7 +1,7 @@ require 'securerandom' module QA - describe 'API basics', :core do + describe 'API basics' do before(:context) do @api_client = Runtime::API::Client.new(:gitlab) end diff --git a/qa/qa/specs/features/api/users_spec.rb b/qa/qa/specs/features/api/users_spec.rb index 8a63d8095c9..2a9360734bb 100644 --- a/qa/qa/specs/features/api/users_spec.rb +++ b/qa/qa/specs/features/api/users_spec.rb @@ -1,5 +1,5 @@ module QA - describe 'API users', :core do + describe 'API users' do before(:context) do @api_client = Runtime::API::Client.new(:gitlab) end diff --git a/qa/qa/specs/features/login/basic_spec.rb b/qa/qa/specs/features/login/basic_spec.rb new file mode 100644 index 00000000000..f866466c7bf --- /dev/null +++ b/qa/qa/specs/features/login/basic_spec.rb @@ -0,0 +1,15 @@ +module QA + describe 'basic user login', :smoke do + it 'user logs in using basic credentials' do + Runtime::Browser.visit(:gitlab, Page::Main::Login) + Page::Main::Login.act { sign_in_using_credentials } + + # TODO, since `Signed in successfully` message was removed + # this is the only way to tell if user is signed in correctly. + # + Page::Menu::Main.perform do |menu| + expect(menu).to have_personal_area + end + end + end +end diff --git a/qa/qa/specs/features/login/ldap_spec.rb b/qa/qa/specs/features/login/ldap_spec.rb index b7a284c584b..de6111eea64 100644 --- a/qa/qa/specs/features/login/ldap_spec.rb +++ b/qa/qa/specs/features/login/ldap_spec.rb @@ -1,5 +1,5 @@ module QA - describe 'LDAP user login', :ldap do + describe 'LDAP user login', :orchestrated, :ldap do before do Runtime::Env.user_type = 'ldap' end diff --git a/qa/qa/specs/features/mattermost/group_create_spec.rb b/qa/qa/specs/features/mattermost/group_create_spec.rb index a59761da341..097e1713aef 100644 --- a/qa/qa/specs/features/mattermost/group_create_spec.rb +++ b/qa/qa/specs/features/mattermost/group_create_spec.rb @@ -1,5 +1,5 @@ module QA - describe 'create a new group', :mattermost do + describe 'create a new group', :orchestrated, :mattermost do it 'creating a group with a mattermost team' do Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.act { sign_in_using_credentials } diff --git a/qa/qa/specs/features/mattermost/login_spec.rb b/qa/qa/specs/features/mattermost/login_spec.rb index b140191e160..27f7d4c245f 100644 --- a/qa/qa/specs/features/mattermost/login_spec.rb +++ b/qa/qa/specs/features/mattermost/login_spec.rb @@ -1,5 +1,5 @@ module QA - describe 'logging in to Mattermost', :mattermost do + describe 'logging in to Mattermost', :orchestrated, :mattermost do it 'can use gitlab oauth' do Runtime::Browser.visit(:gitlab, Page::Main::Login) do Page::Main::Login.act { sign_in_using_credentials } diff --git a/qa/qa/specs/features/merge_request/create_spec.rb b/qa/qa/specs/features/merge_request/create_spec.rb index 36d7efb02e1..71e79956b85 100644 --- a/qa/qa/specs/features/merge_request/create_spec.rb +++ b/qa/qa/specs/features/merge_request/create_spec.rb @@ -1,5 +1,5 @@ module QA - describe 'creates a merge request', :core do + describe 'creates a merge request with milestone' do it 'user creates a new merge request' do Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.act { sign_in_using_credentials } @@ -29,4 +29,25 @@ module QA end end end + + describe 'creates a merge request', :smoke do + it 'user creates a new merge request' do + Runtime::Browser.visit(:gitlab, Page::Main::Login) + Page::Main::Login.act { sign_in_using_credentials } + + current_project = Factory::Resource::Project.fabricate! do |project| + project.name = 'project-with-merge-request' + end + + Factory::Resource::MergeRequest.fabricate! do |merge_request| + merge_request.title = 'This is a merge request' + merge_request.description = 'Great feature' + merge_request.project = current_project + end + + expect(page).to have_content('This is a merge request') + expect(page).to have_content('Great feature') + expect(page).to have_content(/Opened [\w\s]+ ago/) + end + end end diff --git a/qa/qa/specs/features/merge_request/rebase_spec.rb b/qa/qa/specs/features/merge_request/rebase_spec.rb index 163dcbe7963..c36d28e4237 100644 --- a/qa/qa/specs/features/merge_request/rebase_spec.rb +++ b/qa/qa/specs/features/merge_request/rebase_spec.rb @@ -1,5 +1,5 @@ module QA - describe 'merge request rebase', :core do + describe 'merge request rebase' do it 'rebases source branch of merge request' do Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.act { sign_in_using_credentials } diff --git a/qa/qa/specs/features/merge_request/squash_spec.rb b/qa/qa/specs/features/merge_request/squash_spec.rb index 4856bbe1a69..3ecc36a5ae1 100644 --- a/qa/qa/specs/features/merge_request/squash_spec.rb +++ b/qa/qa/specs/features/merge_request/squash_spec.rb @@ -1,5 +1,5 @@ module QA - describe 'merge request squash commits', :core do + describe 'merge request squash commits' do it 'when squash commits is marked before merge' do Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.act { sign_in_using_credentials } diff --git a/qa/qa/specs/features/project/activity_spec.rb b/qa/qa/specs/features/project/activity_spec.rb index 02074e386b6..c7ce8dfdcc6 100644 --- a/qa/qa/specs/features/project/activity_spec.rb +++ b/qa/qa/specs/features/project/activity_spec.rb @@ -1,5 +1,5 @@ module QA - describe 'activity page', :core do + describe 'activity page' do it 'push creates an event in the activity page' do Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.act { sign_in_using_credentials } diff --git a/qa/qa/specs/features/project/add_deploy_key_spec.rb b/qa/qa/specs/features/project/add_deploy_key_spec.rb index 14642af97ad..24f9f4c77f8 100644 --- a/qa/qa/specs/features/project/add_deploy_key_spec.rb +++ b/qa/qa/specs/features/project/add_deploy_key_spec.rb @@ -1,5 +1,5 @@ module QA - describe 'deploy keys support', :core do + describe 'deploy keys support' do it 'user adds a deploy key' do Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.act { sign_in_using_credentials } diff --git a/qa/qa/specs/features/project/add_secret_variable_spec.rb b/qa/qa/specs/features/project/add_secret_variable_spec.rb index 32c91dd9d4d..04d9fe488e2 100644 --- a/qa/qa/specs/features/project/add_secret_variable_spec.rb +++ b/qa/qa/specs/features/project/add_secret_variable_spec.rb @@ -1,5 +1,5 @@ module QA - describe 'secret variables support', :core do + describe 'secret variables support' do it 'user adds a secret variable' do Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.act { sign_in_using_credentials } diff --git a/qa/qa/specs/features/project/auto_devops_spec.rb b/qa/qa/specs/features/project/auto_devops_spec.rb index bc713b46d81..248669b6046 100644 --- a/qa/qa/specs/features/project/auto_devops_spec.rb +++ b/qa/qa/specs/features/project/auto_devops_spec.rb @@ -1,7 +1,7 @@ require 'pathname' module QA - describe 'Auto Devops', :kubernetes do + describe 'Auto Devops', :orchestrated, :kubernetes do after do @cluster&.remove! end @@ -15,6 +15,13 @@ module QA p.description = 'Project with Auto Devops' end + # Disable code_quality check in Auto DevOps pipeline as it takes + # too long and times out the test + Factory::Resource::SecretVariable.fabricate! do |resource| + resource.key = 'CODE_QUALITY_DISABLED' + resource.value = '1' + end + # Create Auto Devops compatible repo Factory::Repository::ProjectPush.fabricate! do |push| push.project = project @@ -50,7 +57,7 @@ module QA Page::Project::Pipeline::Show.perform do |pipeline| expect(pipeline).to have_build('build', status: :success, wait: 600) expect(pipeline).to have_build('test', status: :success, wait: 600) - expect(pipeline).to have_build('production', status: :success, wait: 600) + expect(pipeline).to have_build('production', status: :success, wait: 1200) end end end diff --git a/qa/qa/specs/features/project/create_issue_spec.rb b/qa/qa/specs/features/project/create_issue_spec.rb index ac2ed2ca2a1..793e7db87cb 100644 --- a/qa/qa/specs/features/project/create_issue_spec.rb +++ b/qa/qa/specs/features/project/create_issue_spec.rb @@ -1,5 +1,5 @@ module QA - describe 'creates issue', :core do + describe 'creates issue', :smoke do let(:issue_title) { 'issue title' } it 'user creates issue' do diff --git a/qa/qa/specs/features/project/create_spec.rb b/qa/qa/specs/features/project/create_spec.rb index 14ecd464685..5e19e490778 100644 --- a/qa/qa/specs/features/project/create_spec.rb +++ b/qa/qa/specs/features/project/create_spec.rb @@ -1,5 +1,5 @@ module QA - describe 'create a new project', :core do + describe 'create a new project', :smoke do it 'user creates a new project' do Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.act { sign_in_using_credentials } diff --git a/qa/qa/specs/features/project/deploy_key_clone_spec.rb b/qa/qa/specs/features/project/deploy_key_clone_spec.rb index 054f49b408a..1d099508c24 100644 --- a/qa/qa/specs/features/project/deploy_key_clone_spec.rb +++ b/qa/qa/specs/features/project/deploy_key_clone_spec.rb @@ -1,7 +1,7 @@ require 'digest/sha1' module QA - describe 'cloning code using a deploy key', :core, :docker do + describe 'cloning code using a deploy key', :docker do def login Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.act { sign_in_using_credentials } diff --git a/qa/qa/specs/features/project/fork_project_spec.rb b/qa/qa/specs/features/project/fork_project_spec.rb index 8ad0120305a..d3534d736e4 100644 --- a/qa/qa/specs/features/project/fork_project_spec.rb +++ b/qa/qa/specs/features/project/fork_project_spec.rb @@ -1,5 +1,5 @@ module QA - describe 'Project fork', :core do + describe 'Project fork' do it 'can submit merge requests to upstream master' do Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.act { sign_in_using_credentials } diff --git a/qa/qa/specs/features/project/import_from_github_spec.rb b/qa/qa/specs/features/project/import_from_github_spec.rb index 221b5c27fba..57695d2c726 100644 --- a/qa/qa/specs/features/project/import_from_github_spec.rb +++ b/qa/qa/specs/features/project/import_from_github_spec.rb @@ -1,5 +1,5 @@ module QA - describe 'user imports a GitHub repo', :core, :github do + describe 'user imports a GitHub repo', :orchestrated, :github do let(:imported_project) do Factory::Resource::ProjectImportedFromGithub.fabricate! do |project| project.name = 'imported-project' diff --git a/qa/qa/specs/features/project/pipelines_spec.rb b/qa/qa/specs/features/project/pipelines_spec.rb index ddedde7a8bc..6c6b4e80626 100644 --- a/qa/qa/specs/features/project/pipelines_spec.rb +++ b/qa/qa/specs/features/project/pipelines_spec.rb @@ -1,5 +1,5 @@ module QA - describe 'CI/CD Pipelines', :core, :docker do + describe 'CI/CD Pipelines', :orchestrated, :docker do let(:executor) { "qa-runner-#{Time.now.to_i}" } after do diff --git a/qa/qa/specs/features/project/wikis_spec.rb b/qa/qa/specs/features/project/wikis_spec.rb index 59cc455fffc..9af2dbd1264 100644 --- a/qa/qa/specs/features/project/wikis_spec.rb +++ b/qa/qa/specs/features/project/wikis_spec.rb @@ -1,5 +1,5 @@ module QA - describe 'Wiki Functionality', :core do + describe 'Wiki Functionality' do def login Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.act { sign_in_using_credentials } diff --git a/qa/qa/specs/features/repository/clone_spec.rb b/qa/qa/specs/features/repository/clone_spec.rb index a04ce4e44d9..8b0613c5f78 100644 --- a/qa/qa/specs/features/repository/clone_spec.rb +++ b/qa/qa/specs/features/repository/clone_spec.rb @@ -1,5 +1,5 @@ module QA - describe 'clone code from the repository', :core do + describe 'clone code from the repository' do context 'with regular account over http' do let(:location) do Page::Project::Show.act do diff --git a/qa/qa/specs/features/repository/protected_branches_spec.rb b/qa/qa/specs/features/repository/protected_branches_spec.rb index c2de94516d9..aa23145478d 100644 --- a/qa/qa/specs/features/repository/protected_branches_spec.rb +++ b/qa/qa/specs/features/repository/protected_branches_spec.rb @@ -1,5 +1,5 @@ module QA - describe 'branch protection support', :core do + describe 'branch protection support' do let(:branch_name) { 'protected-branch' } let(:commit_message) { 'Protected push commit message' } let(:project) do diff --git a/qa/qa/specs/features/repository/push_spec.rb b/qa/qa/specs/features/repository/push_spec.rb index fc40b60d915..1e89942e932 100644 --- a/qa/qa/specs/features/repository/push_spec.rb +++ b/qa/qa/specs/features/repository/push_spec.rb @@ -1,5 +1,5 @@ module QA - describe 'push code to repository', :core do + describe 'push code to repository' do context 'with regular account over http' do it 'user pushes code to the repository' do Runtime::Browser.visit(:gitlab, Page::Main::Login) diff --git a/qa/qa/specs/runner.rb b/qa/qa/specs/runner.rb index f8f6fe65599..ccb9d5591de 100644 --- a/qa/qa/specs/runner.rb +++ b/qa/qa/specs/runner.rb @@ -14,7 +14,13 @@ module QA def perform args = [] args.push('--tty') if tty - tags.to_a.each { |tag| args.push(['-t', tag.to_s]) } + + if tags.any? + tags.each { |tag| args.push(['-t', tag.to_s]) } + else + args.push(%w[-t ~orchestrated]) + end + args.push(options) Runtime::Browser.configure! diff --git a/qa/spec/scenario/test/instance_spec.rb b/qa/spec/scenario/test/instance/all_spec.rb index 0d0b534911f..bc0b21c6494 100644 --- a/qa/spec/scenario/test/instance_spec.rb +++ b/qa/spec/scenario/test/instance/all_spec.rb @@ -1,10 +1,4 @@ -describe QA::Scenario::Test::Instance do - subject do - Class.new(described_class) do - tags :rspec - end - end - +describe QA::Scenario::Test::Instance::All do context '#perform' do let(:arguments) { spy('Runtime::Scenario') } let(:release) { spy('Runtime::Release') } @@ -26,16 +20,16 @@ describe QA::Scenario::Test::Instance do end context 'no paths' do - it 'should call runner with default arguments' do + it 'calls runner with default arguments' do subject.perform("test") expect(runner).to have_received(:options=) - .with(::File.expand_path('../../../qa/specs/features', __dir__)) + .with(::File.expand_path('../../../../../qa/specs/features', __dir__)) end end context 'specifying paths' do - it 'should call runner with paths' do + it 'calls runner with paths' do subject.perform('test', 'path1', 'path2') expect(runner).to have_received(:options=).with(%w[path1 path2]) diff --git a/qa/spec/scenario/test/instance/smoke_spec.rb b/qa/spec/scenario/test/instance/smoke_spec.rb new file mode 100644 index 00000000000..66d71610341 --- /dev/null +++ b/qa/spec/scenario/test/instance/smoke_spec.rb @@ -0,0 +1,45 @@ +describe QA::Scenario::Test::Instance::Smoke do + subject { Class.new(described_class) { tags :smoke } } + + context '#perform' do + let(:arguments) { spy('Runtime::Scenario') } + let(:release) { spy('Runtime::Release') } + let(:runner) { spy('Specs::Runner') } + + before do + stub_const('QA::Runtime::Release', release) + stub_const('QA::Runtime::Scenario', arguments) + stub_const('QA::Specs::Runner', runner) + + allow(runner).to receive(:perform).and_yield(runner) + end + + it 'sets an address of the subject' do + subject.perform("hello") + + expect(arguments).to have_received(:define) + .with(:gitlab_address, "hello") + end + + it 'has a smoke tag' do + expect(subject.focus).to eq([:smoke]) # rubocop:disable Focus + end + + context 'no paths' do + it 'calls runner with default arguments' do + subject.perform("test") + + expect(runner).to have_received(:options=) + .with(File.expand_path('../../../../../qa/specs/features', __dir__)) + end + end + + context 'specifying paths' do + it 'calls runner with paths' do + subject.perform('test', 'path1', 'path2') + + expect(runner).to have_received(:options=).with(%w[path1 path2]) + end + end + end +end diff --git a/rubocop/cop/destroy_all.rb b/rubocop/cop/destroy_all.rb new file mode 100644 index 00000000000..38b6cb40f91 --- /dev/null +++ b/rubocop/cop/destroy_all.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module RuboCop + module Cop + # Cop that blacklists the use of `destroy_all`. + class DestroyAll < RuboCop::Cop::Cop + MSG = 'Use `delete_all` instead of `destroy_all`. ' \ + '`destroy_all` will load the rows into memory, then execute a ' \ + '`DELETE` for every individual row.' + + def_node_matcher :destroy_all?, <<~PATTERN + (send {send ivar lvar const} :destroy_all ...) + PATTERN + + def on_send(node) + return unless destroy_all?(node) + + add_offense(node, location: :expression) + end + end + end +end diff --git a/rubocop/cop/migration/add_reference.rb b/rubocop/cop/migration/add_reference.rb new file mode 100644 index 00000000000..4b67270c97a --- /dev/null +++ b/rubocop/cop/migration/add_reference.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true +require_relative '../../migration_helpers' + +module RuboCop + module Cop + module Migration + # Cop that checks if a foreign key constraint is added and require a index for it + class AddReference < RuboCop::Cop::Cop + include MigrationHelpers + + MSG = '`add_reference` requires `index: true`' + + def on_send(node) + return unless in_migration?(node) + + name = node.children[1] + + return unless name == :add_reference + + opts = node.children.last + + add_offense(node, location: :selector) unless opts && opts.type == :hash + + index_present = false + + opts.each_node(:pair) do |pair| + index_present ||= index_enabled?(pair) + end + + add_offense(node, location: :selector) unless index_present + end + + private + + def index_enabled?(pair) + hash_key_type(pair) == :sym && hash_key_name(pair) == :index && pair.children[1].true_type? + end + + def hash_key_type(pair) + pair.children[0].type + end + + def hash_key_name(pair) + pair.children[0].children[0] + end + end + end + end +end diff --git a/rubocop/rubocop.rb b/rubocop/rubocop.rb index aa7ae601f75..88c9bbf24f4 100644 --- a/rubocop/rubocop.rb +++ b/rubocop/rubocop.rb @@ -11,6 +11,7 @@ require_relative 'cop/migration/add_column' require_relative 'cop/migration/add_concurrent_foreign_key' require_relative 'cop/migration/add_concurrent_index' require_relative 'cop/migration/add_index' +require_relative 'cop/migration/add_reference' require_relative 'cop/migration/add_timestamps' require_relative 'cop/migration/datetime' require_relative 'cop/migration/hash_index' @@ -26,3 +27,4 @@ require_relative 'cop/project_path_helper' require_relative 'cop/rspec/env_assignment' require_relative 'cop/rspec/factories_in_migration_specs' require_relative 'cop/sidekiq_options_queue' +require_relative 'cop/destroy_all' diff --git a/scripts/frontend/extract_gettext_all.js b/scripts/frontend/extract_gettext_all.js new file mode 100644 index 00000000000..af8cc4c3341 --- /dev/null +++ b/scripts/frontend/extract_gettext_all.js @@ -0,0 +1,72 @@ +const argumentsParser = require('commander'); + +const { GettextExtractor, JsExtractors } = require('gettext-extractor'); +const { + decorateJSParserWithVueSupport, + decorateExtractorWithHelpers, +} = require('gettext-extractor-vue'); +const ensureSingleLine = require('../../app/assets/javascripts/locale/ensure_single_line.js'); + +const arguments = argumentsParser + .option('-f, --file <file>', 'Extract message from one single file') + .option('-a, --all', 'Extract message from all js/vue files') + .parse(process.argv); + +const extractor = decorateExtractorWithHelpers(new GettextExtractor()); + +extractor.addMessageTransformFunction(ensureSingleLine); + +const jsParser = extractor.createJsParser([ + // Place all the possible expressions to extract here: + JsExtractors.callExpression('__', { + arguments: { + text: 0, + }, + }), + JsExtractors.callExpression('n__', { + arguments: { + text: 0, + textPlural: 1, + }, + }), + JsExtractors.callExpression('s__', { + arguments: { + text: 0, + }, + }), +]); + +const vueParser = decorateJSParserWithVueSupport(jsParser); + +function printJson() { + const messages = extractor.getMessages().reduce((result, message) => { + let text = message.text; + if (message.textPlural) { + text += `\u0000${message.textPlural}`; + } + + message.references.forEach(reference => { + const filename = reference.replace(/:\d+$/, ''); + + if (!Array.isArray(result[filename])) { + result[filename] = []; + } + + result[filename].push([text, reference]); + }); + + return result; + }, {}); + + console.log(JSON.stringify(messages)); +} + +if (arguments.file) { + vueParser.parseFile(arguments.file).then(() => printJson()); +} else if (arguments.all) { + vueParser.parseFilesGlob('{ee/app,app}/assets/javascripts/**/*.{js,vue}').then(() => printJson()); +} else { + console.warn('ERROR: Please use the script correctly:'); + arguments.outputHelp(); + process.exit(1); +} diff --git a/spec/controllers/application_controller_spec.rb b/spec/controllers/application_controller_spec.rb index 421ab006792..fbf116e533b 100644 --- a/spec/controllers/application_controller_spec.rb +++ b/spec/controllers/application_controller_spec.rb @@ -162,6 +162,10 @@ describe ApplicationController do describe 'session expiration' do controller(described_class) do + # The anonymous controller will report 401 and fail to run any actions. + # Normally, GitLab will just redirect you to sign in. + skip_before_action :authenticate_user!, only: :index + def index render text: 'authenticated' end diff --git a/spec/controllers/autocomplete_controller_spec.rb b/spec/controllers/autocomplete_controller_spec.rb index 2c59d1929a1..883bb35f396 100644 --- a/spec/controllers/autocomplete_controller_spec.rb +++ b/spec/controllers/autocomplete_controller_spec.rb @@ -274,14 +274,11 @@ describe AutocompleteController do context 'authorized projects apply limit' do before do - authorized_project2 = create(:project) - authorized_project3 = create(:project) - - authorized_project.add_maintainer(user) - authorized_project2.add_maintainer(user) - authorized_project3.add_maintainer(user) + allow(Kaminari.config).to receive(:default_per_page).and_return(2) - stub_const 'MoveToProjectFinder::PAGE_SIZE', 2 + create_list(:project, 2) do |project| + project.add_maintainer(user) + end end describe 'GET #projects with project ID' do @@ -291,7 +288,7 @@ describe AutocompleteController do it 'returns projects' do expect(json_response).to be_kind_of(Array) - expect(json_response.size).to eq 2 # Of a total of 3 + expect(json_response.size).to eq(Kaminari.config.default_per_page) end end end diff --git a/spec/controllers/notification_settings_controller_spec.rb b/spec/controllers/notification_settings_controller_spec.rb index e133950e684..a3356a86d4b 100644 --- a/spec/controllers/notification_settings_controller_spec.rb +++ b/spec/controllers/notification_settings_controller_spec.rb @@ -21,10 +21,11 @@ describe NotificationSettingsController do end context 'when authorized' do + let(:notification_setting) { user.notification_settings_for(source) } let(:custom_events) do events = {} - NotificationSetting::EMAIL_EVENTS.each do |event| + NotificationSetting.email_events(source).each do |event| events[event.to_s] = true end @@ -36,7 +37,7 @@ describe NotificationSettingsController do end context 'for projects' do - let(:notification_setting) { user.notification_settings_for(project) } + let(:source) { project } it 'creates notification setting' do post :create, @@ -67,7 +68,7 @@ describe NotificationSettingsController do end context 'for groups' do - let(:notification_setting) { user.notification_settings_for(group) } + let(:source) { group } it 'creates notification setting' do post :create, @@ -145,7 +146,7 @@ describe NotificationSettingsController do let(:custom_events) do events = {} - NotificationSetting::EMAIL_EVENTS.each do |event| + notification_setting.email_events.each do |event| events[event] = "true" end end diff --git a/spec/controllers/omniauth_callbacks_controller_spec.rb b/spec/controllers/omniauth_callbacks_controller_spec.rb index b23f183fec8..d377d69457f 100644 --- a/spec/controllers/omniauth_callbacks_controller_spec.rb +++ b/spec/controllers/omniauth_callbacks_controller_spec.rb @@ -95,7 +95,7 @@ describe OmniauthCallbacksController, type: :controller do end it 'allows linking the disabled provider' do - user.identities.destroy_all + user.identities.destroy_all # rubocop: disable DestroyAll sign_in(user) expect { post provider }.to change { user.reload.identities.count }.by(1) diff --git a/spec/controllers/projects/releases_controller_spec.rb b/spec/controllers/projects/releases_controller_spec.rb index fc1619acec6..20a6beb3df8 100644 --- a/spec/controllers/projects/releases_controller_spec.rb +++ b/spec/controllers/projects/releases_controller_spec.rb @@ -14,7 +14,7 @@ describe Projects::ReleasesController do describe 'GET #edit' do it 'initializes a new release' do tag_id = release.tag - project.releases.destroy_all + project.releases.destroy_all # rubocop: disable DestroyAll get :edit, namespace_id: project.namespace, project_id: project, tag_id: tag_id diff --git a/spec/factories/uploads.rb b/spec/factories/uploads.rb index a81b2169b89..81c485fba1a 100644 --- a/spec/factories/uploads.rb +++ b/spec/factories/uploads.rb @@ -46,6 +46,13 @@ FactoryBot.define do secret SecureRandom.hex end + trait :favicon_upload do + model { build(:appearance) } + path { File.join(secret, filename) } + uploader "FaviconUploader" + secret SecureRandom.hex + end + trait :attachment_upload do transient do mount_point :attachment diff --git a/spec/features/admin/admin_runners_spec.rb b/spec/features/admin/admin_runners_spec.rb index be8754a5315..5623e47eadf 100644 --- a/spec/features/admin/admin_runners_spec.rb +++ b/spec/features/admin/admin_runners_spec.rb @@ -20,7 +20,7 @@ describe "Admin Runners" do it 'has all necessary texts' do expect(page).to have_text "Setup a shared Runner manually" - expect(page).to have_text "Runners with last contact more than a minute ago: 1" + expect(page).to have_text "Runners currently online: 1" end describe 'search' do @@ -55,7 +55,7 @@ describe "Admin Runners" do it 'has all necessary texts including no runner message' do expect(page).to have_text "Setup a shared Runner manually" - expect(page).to have_text "Runners with last contact more than a minute ago: 0" + expect(page).to have_text "Runners currently online: 0" expect(page).to have_text 'No runners found' end end diff --git a/spec/features/boards/boards_spec.rb b/spec/features/boards/boards_spec.rb index a0af2dea3ad..baa2b1d8af5 100644 --- a/spec/features/boards/boards_spec.rb +++ b/spec/features/boards/boards_spec.rb @@ -44,7 +44,7 @@ describe 'Issue Boards', :js do end it 'creates default lists' do - lists = ['Backlog', 'To Do', 'Doing', 'Closed'] + lists = ['Open', 'To Do', 'Doing', 'Closed'] page.within(find('.board-blank-state')) do click_button('Add default lists') diff --git a/spec/features/commits/user_uses_slash_commands_spec.rb b/spec/features/commits/user_uses_slash_commands_spec.rb new file mode 100644 index 00000000000..9a4b7bd2444 --- /dev/null +++ b/spec/features/commits/user_uses_slash_commands_spec.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe 'Commit > User uses quick actions', :js do + include Spec::Support::Helpers::Features::NotesHelpers + include RepoHelpers + + let(:project) { create(:project, :public, :repository) } + let(:user) { project.creator } + let(:commit) { project.commit } + + before do + project.add_maintainer(user) + sign_in(user) + + visit project_commit_path(project, commit.id) + end + + describe 'Tagging a commit' do + let(:tag_name) { 'v1.2.3' } + let(:tag_message) { 'Stable release' } + let(:truncated_commit_sha) { Commit.truncate_sha(commit.sha) } + + it 'tags this commit' do + add_note("/tag #{tag_name} #{tag_message}") + + expect(page).to have_content 'Commands applied' + expect(page).to have_content "tagged commit #{truncated_commit_sha}" + expect(page).to have_content tag_name + + visit project_tag_path(project, tag_name) + expect(page).to have_content tag_name + expect(page).to have_content tag_message + expect(page).to have_content truncated_commit_sha + end + + describe 'preview', :js do + it 'removes quick action from note and explains it' do + preview_note("/tag #{tag_name} #{tag_message}") + + expect(page).not_to have_content '/tag' + expect(page).to have_content %{Tags this commit to #{tag_name} with "#{tag_message}"} + expect(page).to have_content tag_name + end + end + end +end diff --git a/spec/features/instance_statistics/cohorts_spec.rb b/spec/features/instance_statistics/cohorts_spec.rb index 81fc5eff980..573f8600be1 100644 --- a/spec/features/instance_statistics/cohorts_spec.rb +++ b/spec/features/instance_statistics/cohorts_spec.rb @@ -12,4 +12,12 @@ describe 'Cohorts page' do expect(page).to have_content("#{Time.now.strftime('%b %Y')} 3 0") end + + it 'shows usage data', :js do + visit instance_statistics_cohorts_path + + wait_for_requests + + expect(find('.js-syntax-highlight').text).not_to eq('') + end end diff --git a/spec/features/instance_statistics/conversational_development_index_spec.rb b/spec/features/instance_statistics/conversational_development_index_spec.rb index d441a7a5af9..a6c16b6a2a3 100644 --- a/spec/features/instance_statistics/conversational_development_index_spec.rb +++ b/spec/features/instance_statistics/conversational_development_index_spec.rb @@ -5,6 +5,16 @@ describe 'Conversational Development Index' do sign_in(create(:admin)) end + it 'has dismissable intro callout', :js do + visit instance_statistics_conversational_development_index_index_path + + expect(page).to have_content 'Introducing Your Conversational Development Index' + + find('.js-close-callout').click + + expect(page).not_to have_content 'Introducing Your Conversational Development Index' + end + context 'when usage ping is disabled' do it 'shows empty state' do stub_application_setting(usage_ping_enabled: false) diff --git a/spec/features/issues/award_emoji_spec.rb b/spec/features/issues/award_emoji_spec.rb deleted file mode 100644 index bf60b18873c..00000000000 --- a/spec/features/issues/award_emoji_spec.rb +++ /dev/null @@ -1,146 +0,0 @@ -require 'rails_helper' - -describe 'Awards Emoji' do - let!(:project) { create(:project, :public) } - let!(:user) { create(:user) } - let(:issue) do - create(:issue, - assignees: [user], - project: project) - end - - context 'authorized user' do - before do - project.add_maintainer(user) - sign_in(user) - end - - describe 'visiting an issue with a legacy award emoji that is not valid anymore' do - before do - # The `heart_tip` emoji is not valid anymore so we need to skip validation - issue.award_emoji.build(user: user, name: 'heart_tip').save!(validate: false) - visit project_issue_path(project, issue) - wait_for_requests - end - - # Regression test: https://gitlab.com/gitlab-org/gitlab-ce/issues/29529 - it 'does not shows a 500 page', :js do - expect(page).to have_text(issue.title) - end - end - - describe 'Click award emoji from issue#show' do - let!(:note) { create(:note_on_issue, noteable: issue, project: issue.project, note: "Hello world") } - - before do - visit project_issue_path(project, issue) - wait_for_requests - end - - it 'increments the thumbsdown emoji', :js do - find('[data-name="thumbsdown"]').click - wait_for_requests - expect(thumbsdown_emoji).to have_text("1") - end - - context 'click the thumbsup emoji' do - it 'increments the thumbsup emoji', :js do - find('[data-name="thumbsup"]').click - wait_for_requests - expect(thumbsup_emoji).to have_text("1") - end - - it 'decrements the thumbsdown emoji', :js do - expect(thumbsdown_emoji).to have_text("0") - end - end - - context 'click the thumbsdown emoji' do - it 'increments the thumbsdown emoji', :js do - find('[data-name="thumbsdown"]').click - wait_for_requests - expect(thumbsdown_emoji).to have_text("1") - end - - it 'decrements the thumbsup emoji', :js do - expect(thumbsup_emoji).to have_text("0") - end - end - - it 'toggles the smiley emoji on a note', :js do - toggle_smiley_emoji(true) - - within('.note-body') do - expect(find(emoji_counter)).to have_text("1") - end - - toggle_smiley_emoji(false) - - within('.note-body') do - expect(page).not_to have_selector(emoji_counter) - end - end - - context 'execute /award quick action' do - it 'toggles the emoji award on noteable', :js do - execute_quick_action('/award :100:') - - expect(find(noteable_award_counter)).to have_text("1") - - execute_quick_action('/award :100:') - - expect(page).not_to have_selector(noteable_award_counter) - end - end - end - end - - context 'unauthorized user', :js do - before do - visit project_issue_path(project, issue) - end - - it 'has disabled emoji button' do - expect(first('.award-control')[:class]).to have_text('disabled') - end - end - - def execute_quick_action(cmd) - within('.js-main-target-form') do - fill_in 'note[note]', with: cmd - click_button 'Comment' - end - - wait_for_requests - end - - def thumbsup_emoji - page.all(emoji_counter).first - end - - def thumbsdown_emoji - page.all(emoji_counter).last - end - - def emoji_counter - 'span.js-counter' - end - - def noteable_award_counter - ".awards .active" - end - - def toggle_smiley_emoji(status) - within('.note') do - find('.note-emoji-button').click - end - - unless status - first('[data-name="smiley"]').click - else - find('[data-name="smiley"]').click - end - - wait_for_requests - end -end diff --git a/spec/features/issues/award_spec.rb b/spec/features/issues/award_spec.rb deleted file mode 100644 index e53a4ce49c7..00000000000 --- a/spec/features/issues/award_spec.rb +++ /dev/null @@ -1,51 +0,0 @@ -require 'rails_helper' - -describe 'Issue awards', :js do - let(:user) { create(:user) } - let(:project) { create(:project, :public) } - let(:issue) { create(:issue, project: project) } - - describe 'logged in' do - before do - sign_in(user) - visit project_issue_path(project, issue) - wait_for_requests - end - - it 'adds award to issue' do - first('.js-emoji-btn').click - expect(page).to have_selector('.js-emoji-btn.active') - expect(first('.js-emoji-btn')).to have_content '1' - - visit project_issue_path(project, issue) - expect(first('.js-emoji-btn')).to have_content '1' - end - - it 'removes award from issue' do - first('.js-emoji-btn').click - find('.js-emoji-btn.active').click - expect(first('.js-emoji-btn')).to have_content '0' - - visit project_issue_path(project, issue) - expect(first('.js-emoji-btn')).to have_content '0' - end - - it 'only has one menu on the page' do - first('.js-add-award').click - expect(page).to have_selector('.emoji-menu') - - expect(page).to have_selector('.emoji-menu', count: 1) - end - end - - describe 'logged out' do - before do - visit project_issue_path(project, issue) - wait_for_requests - end - - it 'does not see award menu button' do - expect(page).not_to have_selector('.js-award-holder') - end - end -end diff --git a/spec/features/issues/user_interacts_with_awards_spec.rb b/spec/features/issues/user_interacts_with_awards_spec.rb new file mode 100644 index 00000000000..afa425c2cec --- /dev/null +++ b/spec/features/issues/user_interacts_with_awards_spec.rb @@ -0,0 +1,347 @@ +require 'spec_helper' + +describe 'User interacts with awards' do + let(:user) { create(:user) } + + describe 'User interacts with awards in an issue', :js do + let(:issue) { create(:issue, project: project)} + let(:project) { create(:project) } + + before do + project.add_maintainer(user) + sign_in(user) + + visit(project_issue_path(project, issue)) + end + + it 'toggles the thumbsup award emoji' do + page.within('.awards') do + thumbsup = page.first('.award-control') + thumbsup.click + thumbsup.hover + + expect(page).to have_selector('.js-emoji-btn') + expect(page).to have_css(".js-emoji-btn.active[data-original-title='You']") + expect(page.find('.js-emoji-btn.active .js-counter')).to have_content('1') + + thumbsup = page.first('.award-control') + thumbsup.click + thumbsup.hover + + expect(page).to have_selector('.award-control.js-emoji-btn') + expect(page.all('.award-control.js-emoji-btn').size).to eq(2) + + page.all('.award-control.js-emoji-btn').each do |element| + expect(element['title']).to eq('') + end + + expect(page.all('.award-control .js-counter')).to all(have_content('0')) + + thumbsup = page.first('.award-control') + thumbsup.click + thumbsup.hover + + expect(page).to have_selector('.js-emoji-btn') + expect(page).to have_css(".js-emoji-btn.active[data-original-title='You']") + expect(page.find('.js-emoji-btn.active .js-counter')).to have_content('1') + end + end + + it 'toggles a custom award emoji' do + page.within('.awards') do + page.find('.js-add-award').click + end + + page.find('.emoji-menu.is-visible') + + expect(page).to have_selector('.js-emoji-menu-search') + expect(page.evaluate_script("document.activeElement.classList.contains('js-emoji-menu-search')")).to eq(true) + + page.within('.emoji-menu-content') do + emoji_button = page.first('.js-emoji-btn') + emoji_button.hover + emoji_button.click + end + + page.within('.awards') do + expect(page).to have_selector('.js-emoji-btn') + expect(page.find('.js-emoji-btn.active .js-counter')).to have_content('1') + expect(page).to have_css(".js-emoji-btn.active[data-original-title='You']") + + expect do + page.find('.js-emoji-btn.active').click + wait_for_requests + end.to change { page.all('.award-control.js-emoji-btn').size }.from(3).to(2) + end + end + + it 'shows the list of award emoji categories' do + page.within('.awards') do + page.find('.js-add-award').click + end + + page.find('.emoji-menu.is-visible') + + expect(page).to have_selector('.js-emoji-menu-search') + expect(page.evaluate_script("document.activeElement.classList.contains('js-emoji-menu-search')")).to eq(true) + + fill_in('emoji-menu-search', with: 'hand') + + page.within('.emoji-menu-content') do + expect(page).to have_selector('[data-name="raised_hand"]') + end + end + + it 'adds an award emoji by a comment' do + page.within('.js-main-target-form') do + fill_in('note[note]', with: ':smile:') + + click_button('Comment') + end + + expect(page).to have_emoji('smile') + end + + context 'when a project is archived' do + let(:project) { create(:project, :archived) } + + it 'hides the add award button' do + page.within('.awards') do + expect(page).not_to have_css('.js-add-award') + end + end + end + + context 'User interacts with awards on a note' do + let!(:note) { create(:note, noteable: issue, project: issue.project) } + let!(:award_emoji) { create(:award_emoji, awardable: note, name: '100') } + + it 'shows the award on the note' do + page.within('.note-awards') do + expect(page).to have_emoji('100') + end + end + + it 'allows adding a vote to an award' do + page.within('.note-awards') do + find('gl-emoji[data-name="100"]').click + end + wait_for_requests + + expect(note.reload.award_emoji.size).to eq(2) + end + + it 'allows adding a new emoji' do + page.within('.note-actions') do + find('a.js-add-award').click + end + page.within('.emoji-menu-content') do + find('gl-emoji[data-name="8ball"]').click + end + wait_for_requests + + page.within('.note-awards') do + expect(page).to have_emoji('8ball') + end + expect(note.reload.award_emoji.size).to eq(2) + end + + context 'when the project is archived' do + let(:project) { create(:project, :archived) } + + it 'hides the buttons for adding new emoji' do + page.within('.note-awards') do + expect(page).not_to have_css('.award-menu-holder') + end + + page.within('.note-actions') do + expect(page).not_to have_css('a.js-add-award') + end + end + + it 'does not allow toggling existing emoji' do + page.within('.note-awards') do + find('gl-emoji[data-name="100"]').click + end + wait_for_requests + + expect(note.reload.award_emoji.size).to eq(1) + end + end + end + end + + describe 'User interacts with awards on an issue', :js do + let(:project) { create(:project, :public) } + let(:issue) { create(:issue, project: project) } + + describe 'logged in' do + before do + sign_in(user) + visit project_issue_path(project, issue) + wait_for_requests + end + + it 'adds award to issue' do + first('.js-emoji-btn').click + + expect(page).to have_selector('.js-emoji-btn.active') + expect(first('.js-emoji-btn')).to have_content '1' + + visit project_issue_path(project, issue) + + expect(first('.js-emoji-btn')).to have_content '1' + end + + it 'removes award from issue' do + first('.js-emoji-btn').click + find('.js-emoji-btn.active').click + + expect(first('.js-emoji-btn')).to have_content '0' + + visit project_issue_path(project, issue) + + expect(first('.js-emoji-btn')).to have_content '0' + end + + it 'only has one menu on the page' do + first('.js-add-award').click + + expect(page).to have_selector('.emoji-menu', count: 1) + end + end + + describe 'logged out' do + before do + visit project_issue_path(project, issue) + wait_for_requests + end + + it 'does not see award menu button' do + expect(page).not_to have_selector('.js-award-holder') + end + end + end + + describe 'Awards Emoji' do + let!(:project) { create(:project, :public) } + let(:issue) { create(:issue, assignees: [user], project: project) } + + context 'authorized user' do + before do + project.add_maintainer(user) + sign_in(user) + end + + describe 'visiting an issue with a legacy award emoji that is not valid anymore' do + before do + # The `heart_tip` emoji is not valid anymore so we need to skip validation + issue.award_emoji.build(user: user, name: 'heart_tip').save!(validate: false) + visit project_issue_path(project, issue) + wait_for_requests + end + + # Regression test: https://gitlab.com/gitlab-org/gitlab-ce/issues/29529 + it 'does not shows a 500 page', :js do + expect(page).to have_text(issue.title) + end + end + + describe 'Click award emoji from issue#show' do + let!(:note) { create(:note_on_issue, noteable: issue, project: issue.project, note: "Hello world") } + + before do + visit project_issue_path(project, issue) + wait_for_requests + end + + context 'click the thumbsdown emoji' do + it 'increments the thumbsdown emoji', :js do + find('[data-name="thumbsdown"]').click + wait_for_requests + expect(thumbsdown_emoji).to have_text("1") + end + + it 'decrements the thumbsup emoji', :js do + expect(thumbsup_emoji).to have_text("0") + end + end + + it 'toggles the smiley emoji on a note', :js do + toggle_smiley_emoji(true) + + within('.note-body') do + expect(find(emoji_counter)).to have_text("1") + end + + toggle_smiley_emoji(false) + + within('.note-body') do + expect(page).not_to have_selector(emoji_counter) + end + end + + context 'execute /award quick action' do + it 'toggles the emoji award on noteable', :js do + execute_quick_action('/award :100:') + + expect(find(noteable_award_counter)).to have_text("1") + + execute_quick_action('/award :100:') + + expect(page).not_to have_selector(noteable_award_counter) + end + end + end + end + + context 'unauthorized user', :js do + before do + visit project_issue_path(project, issue) + end + + it 'has disabled emoji button' do + expect(first('.award-control')[:class]).to have_text('disabled') + end + end + + def execute_quick_action(cmd) + within('.js-main-target-form') do + fill_in 'note[note]', with: cmd + click_button 'Comment' + end + + wait_for_requests + end + + def thumbsup_emoji + page.all(emoji_counter).first + end + + def thumbsdown_emoji + page.all(emoji_counter).last + end + + def emoji_counter + 'span.js-counter' + end + + def noteable_award_counter + ".awards .active" + end + + def toggle_smiley_emoji(status) + within('.note') do + find('.note-emoji-button').click + end + + if !status + first('[data-name="smiley"]').click + else + find('[data-name="smiley"]').click + end + + wait_for_requests + end + end +end diff --git a/spec/features/merge_request/user_sees_mr_with_deleted_source_branch_spec.rb b/spec/features/merge_request/user_sees_mr_with_deleted_source_branch_spec.rb index c1608be402a..fd4175d5227 100644 --- a/spec/features/merge_request/user_sees_mr_with_deleted_source_branch_spec.rb +++ b/spec/features/merge_request/user_sees_mr_with_deleted_source_branch_spec.rb @@ -28,7 +28,7 @@ describe 'Merge request > User sees MR with deleted source branch', :js do click_on 'Changes' wait_for_requests - expect(page).to have_selector('.diffs.tab-pane .nothing-here-block') + expect(page).to have_selector('.diffs.tab-pane .file-holder') expect(page).to have_content('Source branch does not exist.') end end diff --git a/spec/features/projects/awards/user_interacts_with_awards_in_issue_spec.rb b/spec/features/projects/awards/user_interacts_with_awards_in_issue_spec.rb deleted file mode 100644 index 4d860893abe..00000000000 --- a/spec/features/projects/awards/user_interacts_with_awards_in_issue_spec.rb +++ /dev/null @@ -1,172 +0,0 @@ -require 'spec_helper' - -describe 'User interacts with awards in an issue', :js do - let(:issue) { create(:issue, project: project)} - let(:project) { create(:project) } - let(:user) { create(:user) } - - before do - project.add_maintainer(user) - sign_in(user) - - visit(project_issue_path(project, issue)) - end - - it 'toggles the thumbsup award emoji' do - page.within('.awards') do - thumbsup = page.first('.award-control') - thumbsup.click - thumbsup.hover - - expect(page).to have_selector('.js-emoji-btn') - expect(page).to have_css(".js-emoji-btn.active[data-original-title='You']") - expect(page.find('.js-emoji-btn.active .js-counter')).to have_content('1') - - thumbsup = page.first('.award-control') - thumbsup.click - thumbsup.hover - - expect(page).to have_selector('.award-control.js-emoji-btn') - expect(page.all('.award-control.js-emoji-btn').size).to eq(2) - - page.all('.award-control.js-emoji-btn').each do |element| - expect(element['title']).to eq('') - end - - page.all('.award-control .js-counter').each do |element| - expect(element).to have_content('0') - end - - thumbsup = page.first('.award-control') - thumbsup.click - thumbsup.hover - - expect(page).to have_selector('.js-emoji-btn') - expect(page).to have_css(".js-emoji-btn.active[data-original-title='You']") - expect(page.find('.js-emoji-btn.active .js-counter')).to have_content('1') - end - end - - it 'toggles a custom award emoji' do - page.within('.awards') do - page.find('.js-add-award').click - end - - page.find('.emoji-menu.is-visible') - - expect(page).to have_selector('.js-emoji-menu-search') - expect(page.evaluate_script("document.activeElement.classList.contains('js-emoji-menu-search')")).to eq(true) - - page.within('.emoji-menu-content') do - emoji_button = page.first('.js-emoji-btn') - emoji_button.hover - emoji_button.click - end - - page.within('.awards') do - expect(page).to have_selector('.js-emoji-btn') - expect(page.find('.js-emoji-btn.active .js-counter')).to have_content('1') - expect(page).to have_css(".js-emoji-btn.active[data-original-title='You']") - - expect do - page.find('.js-emoji-btn.active').click - wait_for_requests - end.to change { page.all('.award-control.js-emoji-btn').size }.from(3).to(2) - end - end - - it 'shows the list of award emoji categories' do - page.within('.awards') do - page.find('.js-add-award').click - end - - page.find('.emoji-menu.is-visible') - - expect(page).to have_selector('.js-emoji-menu-search') - expect(page.evaluate_script("document.activeElement.classList.contains('js-emoji-menu-search')")).to eq(true) - - fill_in('emoji-menu-search', with: 'hand') - - page.within('.emoji-menu-content') do - expect(page).to have_selector('[data-name="raised_hand"]') - end - end - - it 'adds an award emoji by a comment' do - page.within('.js-main-target-form') do - fill_in('note[note]', with: ':smile:') - - click_button('Comment') - end - - expect(page).to have_emoji('smile') - end - - context 'when a project is archived' do - let(:project) { create(:project, :archived) } - - it 'hides the add award button' do - page.within('.awards') do - expect(page).not_to have_css('.js-add-award') - end - end - end - - context 'awards on a note' do - let!(:note) { create(:note, noteable: issue, project: issue.project) } - let!(:award_emoji) { create(:award_emoji, awardable: note, name: '100') } - - it 'shows the award on the note' do - page.within('.note-awards') do - expect(page).to have_emoji('100') - end - end - - it 'allows adding a vote to an award' do - page.within('.note-awards') do - find('gl-emoji[data-name="100"]').click - end - wait_for_requests - - expect(note.reload.award_emoji.size).to eq(2) - end - - it 'allows adding a new emoji' do - page.within('.note-actions') do - find('a.js-add-award').click - end - page.within('.emoji-menu-content') do - find('gl-emoji[data-name="8ball"]').click - end - wait_for_requests - - page.within('.note-awards') do - expect(page).to have_emoji('8ball') - end - expect(note.reload.award_emoji.size).to eq(2) - end - - context 'when the project is archived' do - let(:project) { create(:project, :archived) } - - it 'hides the buttons for adding new emoji' do - page.within('.note-awards') do - expect(page).not_to have_css('.award-menu-holder') - end - - page.within('.note-actions') do - expect(page).not_to have_css('a.js-add-award') - end - end - - it 'does not allow toggling existing emoji' do - page.within('.note-awards') do - find('gl-emoji[data-name="100"]').click - end - wait_for_requests - - expect(note.reload.award_emoji.size).to eq(1) - end - end - end -end diff --git a/spec/features/projects/blobs/shortcuts_blob_spec.rb b/spec/features/projects/blobs/shortcuts_blob_spec.rb index aeed38aeb76..3925de6cfb9 100644 --- a/spec/features/projects/blobs/shortcuts_blob_spec.rb +++ b/spec/features/projects/blobs/shortcuts_blob_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe 'Blob shortcuts' do +describe 'Blob shortcuts', :js do include TreeHelper let(:project) { create(:project, :public, :repository) } let(:path) { project.repository.ls_files(project.repository.root_ref)[0] } @@ -18,6 +18,7 @@ describe 'Blob shortcuts' do describe 'pressing "y"' do it 'redirects to permalink with commit sha' do visit_blob + wait_for_requests find('body').native.send_key('y') @@ -27,6 +28,7 @@ describe 'Blob shortcuts' do it 'maintains fragment hash when redirecting' do fragment = "L1" visit_blob(fragment) + wait_for_requests find('body').native.send_key('y') diff --git a/spec/features/projects/files/user_browses_files_spec.rb b/spec/features/projects/files/user_browses_files_spec.rb index f56174fc85c..f3cf3a282e5 100644 --- a/spec/features/projects/files/user_browses_files_spec.rb +++ b/spec/features/projects/files/user_browses_files_spec.rb @@ -210,9 +210,10 @@ describe "User browses files" do end end - context "when browsing a file content" do + context "when browsing a file content", :js do before do visit(tree_path_root_ref) + wait_for_requests click_link(".gitignore") end diff --git a/spec/features/projects/files/user_deletes_files_spec.rb b/spec/features/projects/files/user_deletes_files_spec.rb index 0e9f83a16ce..dcb7b947c61 100644 --- a/spec/features/projects/files/user_deletes_files_spec.rb +++ b/spec/features/projects/files/user_deletes_files_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe 'Projects > Files > User deletes files' do +describe 'Projects > Files > User deletes files', :js do let(:fork_message) do "You're not allowed to make changes to this project directly. "\ "A fork of this project has been created that you can make changes in, so you can submit a merge request." @@ -19,6 +19,7 @@ describe 'Projects > Files > User deletes files' do before do project.add_maintainer(user) visit(project_tree_path_root_ref) + wait_for_requests end it 'deletes the file', :js do @@ -35,10 +36,11 @@ describe 'Projects > Files > User deletes files' do end end - context 'when an user does not have write access' do + context 'when an user does not have write access', :js do before do project2.add_reporter(user) visit(project2_tree_path_root_ref) + wait_for_requests end it 'deletes the file in a forked project', :js do diff --git a/spec/features/projects/files/user_edits_files_spec.rb b/spec/features/projects/files/user_edits_files_spec.rb index ccc1bc1bc10..9eb65ec159c 100644 --- a/spec/features/projects/files/user_edits_files_spec.rb +++ b/spec/features/projects/files/user_edits_files_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe 'Projects > Files > User edits files' do +describe 'Projects > Files > User edits files', :js do include ProjectForksHelper let(:project) { create(:project, :repository, name: 'Shop') } let(:project2) { create(:project, :repository, name: 'Another Project', path: 'another-project') } @@ -29,13 +29,14 @@ describe 'Projects > Files > User edits files' do end end - context 'when an user has write access' do + context 'when an user has write access', :js do before do project.add_maintainer(user) visit(project_tree_path_root_ref) + wait_for_requests end - it 'inserts a content of a file', :js do + it 'inserts a content of a file' do click_link('.gitignore') find('.js-edit-blob').click find('.file-editor', match: :first) @@ -49,13 +50,14 @@ describe 'Projects > Files > User edits files' do it 'does not show the edit link if a file is binary' do binary_file = File.join(project.repository.root_ref, 'files/images/logo-black.png') visit(project_blob_path(project, binary_file)) + wait_for_requests page.within '.content' do expect(page).not_to have_link('edit') end end - it 'commits an edited file', :js do + it 'commits an edited file' do click_link('.gitignore') find('.js-edit-blob').click find('.file-editor', match: :first) @@ -72,7 +74,7 @@ describe 'Projects > Files > User edits files' do expect(page).to have_content('*.rbca') end - it 'commits an edited file to a new branch', :js do + it 'commits an edited file to a new branch' do click_link('.gitignore') find('.js-edit-blob').click @@ -91,7 +93,7 @@ describe 'Projects > Files > User edits files' do expect(page).to have_content('*.rbca') end - it 'shows the diff of an edited file', :js do + it 'shows the diff of an edited file' do click_link('.gitignore') find('.js-edit-blob').click find('.file-editor', match: :first) @@ -106,13 +108,14 @@ describe 'Projects > Files > User edits files' do it_behaves_like 'unavailable for an archived project' end - context 'when an user does not have write access' do + context 'when an user does not have write access', :js do before do project2.add_reporter(user) visit(project2_tree_path_root_ref) + wait_for_requests end - it 'inserts a content of a file in a forked project', :js do + it 'inserts a content of a file in a forked project' do click_link('.gitignore') find('.js-edit-blob').click @@ -134,7 +137,7 @@ describe 'Projects > Files > User edits files' do expect(evaluate_script('ace.edit("editor").getValue()')).to eq('*.rbca') end - it 'commits an edited file in a forked project', :js do + it 'commits an edited file in a forked project' do click_link('.gitignore') find('.js-edit-blob').click @@ -163,6 +166,7 @@ describe 'Projects > Files > User edits files' do let!(:forked_project) { fork_project(project2, user, namespace: user.namespace, repository: true) } before do visit(project2_tree_path_root_ref) + wait_for_requests end it 'links to the forked project for editing' do diff --git a/spec/features/projects/files/user_replaces_files_spec.rb b/spec/features/projects/files/user_replaces_files_spec.rb index 3a81e77c4ba..e3da28d73c3 100644 --- a/spec/features/projects/files/user_replaces_files_spec.rb +++ b/spec/features/projects/files/user_replaces_files_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe 'Projects > Files > User replaces files' do +describe 'Projects > Files > User replaces files', :js do include DropzoneHelper let(:fork_message) do @@ -21,9 +21,10 @@ describe 'Projects > Files > User replaces files' do before do project.add_maintainer(user) visit(project_tree_path_root_ref) + wait_for_requests end - it 'replaces an existed file with a new one', :js do + it 'replaces an existed file with a new one' do click_link('.gitignore') expect(page).to have_content('.gitignore') @@ -47,9 +48,10 @@ describe 'Projects > Files > User replaces files' do before do project2.add_reporter(user) visit(project2_tree_path_root_ref) + wait_for_requests end - it 'replaces an existed file with a new one in a forked project', :js do + it 'replaces an existed file with a new one in a forked project' do click_link('.gitignore') expect(page).to have_content('.gitignore') diff --git a/spec/features/projects/remote_mirror_spec.rb b/spec/features/projects/remote_mirror_spec.rb index 5259a8942dc..33e9b73efe8 100644 --- a/spec/features/projects/remote_mirror_spec.rb +++ b/spec/features/projects/remote_mirror_spec.rb @@ -17,7 +17,7 @@ describe 'Project remote mirror', :feature do visit project_mirror_path(project) - expect(page).to have_content('The remote repository failed to update.') + expect_mirror_to_have_error_and_timeago('Never') end end @@ -27,8 +27,14 @@ describe 'Project remote mirror', :feature do visit project_mirror_path(project) - expect(page).to have_content('The remote repository failed to update 5 minutes ago.') + expect_mirror_to_have_error_and_timeago('5 minutes ago') end end + + def expect_mirror_to_have_error_and_timeago(timeago) + row = first('.js-mirrors-table-body tr') + expect(row).to have_content('Error') + expect(row).to have_content(timeago) + end end end diff --git a/spec/features/projects/settings/repository_settings_spec.rb b/spec/features/projects/settings/repository_settings_spec.rb index a0f5b234ebc..377a75cbcb3 100644 --- a/spec/features/projects/settings/repository_settings_spec.rb +++ b/spec/features/projects/settings/repository_settings_spec.rb @@ -129,9 +129,8 @@ describe 'Projects > Settings > Repository settings' do visit project_settings_repository_path(project) end - it 'shows push mirror settings' do - expect(page).to have_selector('#project_remote_mirrors_attributes_0_enabled') - expect(page).to have_selector('#project_remote_mirrors_attributes_0_url') + it 'shows push mirror settings', :js do + expect(page).to have_selector('#mirror_direction') end end end diff --git a/spec/finders/autocomplete/group_finder_spec.rb b/spec/finders/autocomplete/group_finder_spec.rb new file mode 100644 index 00000000000..d7cb2c3bbe2 --- /dev/null +++ b/spec/finders/autocomplete/group_finder_spec.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Autocomplete::GroupFinder do + let(:user) { create(:user) } + + describe '#execute' do + context 'with a project' do + it 'returns nil' do + project = create(:project) + + expect(described_class.new(user, project).execute).to be_nil + end + end + + context 'without a group ID' do + it 'returns nil' do + expect(described_class.new(user).execute).to be_nil + end + end + + context 'with an empty String as the group ID' do + it 'returns nil' do + expect(described_class.new(user, nil, group_id: '').execute).to be_nil + end + end + + context 'without a project and with a group ID' do + it 'raises ActiveRecord::RecordNotFound if the group does not exist' do + finder = described_class.new(user, nil, group_id: 1) + + expect { finder.execute }.to raise_error(ActiveRecord::RecordNotFound) + end + + it 'raises ActiveRecord::RecordNotFound if the user can not read the group' do + group = create(:group, :private) + finder = described_class.new(user, nil, group_id: group.id) + + expect { finder.execute }.to raise_error(ActiveRecord::RecordNotFound) + end + + it 'raises ActiveRecord::RecordNotFound if an anonymous user can not read the group' do + group = create(:group, :private) + finder = described_class.new(nil, nil, group_id: group.id) + + expect { finder.execute }.to raise_error(ActiveRecord::RecordNotFound) + end + + it 'returns the group if it exists and is readable' do + group = create(:group) + finder = described_class.new(user, nil, group_id: group.id) + + expect(finder.execute).to eq(group) + end + end + end +end diff --git a/spec/finders/move_to_project_finder_spec.rb b/spec/finders/autocomplete/move_to_project_finder_spec.rb index 1b3f44cced1..c3bc410a7f6 100644 --- a/spec/finders/move_to_project_finder_spec.rb +++ b/spec/finders/autocomplete/move_to_project_finder_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe MoveToProjectFinder do +describe Autocomplete::MoveToProjectFinder do let(:user) { create(:user) } let(:project) { create(:project) } @@ -10,14 +10,14 @@ describe MoveToProjectFinder do let(:developer_project) { create(:project) } let(:maintainer_project) { create(:project) } - subject { described_class.new(user) } - describe '#execute' do context 'filter' do it 'does not return projects under Gitlab::Access::REPORTER' do guest_project.add_guest(user) - expect(subject.execute(project)).to be_empty + finder = described_class.new(user, project_id: project.id) + + expect(finder.execute).to be_empty end it 'returns projects equal or above Gitlab::Access::REPORTER ordered by id in descending order' do @@ -25,13 +25,17 @@ describe MoveToProjectFinder do developer_project.add_developer(user) maintainer_project.add_maintainer(user) - expect(subject.execute(project).to_a).to eq([maintainer_project, developer_project, reporter_project]) + finder = described_class.new(user, project_id: project.id) + + expect(finder.execute.to_a).to eq([maintainer_project, developer_project, reporter_project]) end it 'does not include the source project' do project.add_reporter(user) - expect(subject.execute(project).to_a).to be_empty + finder = described_class.new(user, project_id: project.id) + + expect(finder.execute.to_a).to be_empty end it 'does not return archived projects' do @@ -40,7 +44,9 @@ describe MoveToProjectFinder do other_reporter_project = create(:project) other_reporter_project.add_reporter(user) - expect(subject.execute(project).to_a).to eq([other_reporter_project]) + finder = described_class.new(user, project_id: project.id) + + expect(finder.execute.to_a).to eq([other_reporter_project]) end it 'does not return projects for which issues are disabled' do @@ -49,39 +55,42 @@ describe MoveToProjectFinder do other_reporter_project = create(:project) other_reporter_project.add_reporter(user) - expect(subject.execute(project).to_a).to eq([other_reporter_project]) + finder = described_class.new(user, project_id: project.id) + + expect(finder.execute.to_a).to eq([other_reporter_project]) end it 'returns a page of projects ordered by id in descending order' do - stub_const 'MoveToProjectFinder::PAGE_SIZE', 2 + allow(Kaminari.config).to receive(:default_per_page).and_return(2) - reporter_project.add_reporter(user) - developer_project.add_developer(user) - maintainer_project.add_maintainer(user) + projects = create_list(:project, 2) do |project| + project.add_developer(user) + end - expect(subject.execute(project).to_a).to eq([maintainer_project, developer_project]) + finder = described_class.new(user, project_id: project.id) + page = finder.execute.to_a + + expect(page.length).to eq(Kaminari.config.default_per_page) + expect(page[0]).to eq(projects.last) end it 'returns projects after the given offset id' do - stub_const 'MoveToProjectFinder::PAGE_SIZE', 2 - reporter_project.add_reporter(user) developer_project.add_developer(user) maintainer_project.add_maintainer(user) - expect(subject.execute(project, search: nil, offset_id: maintainer_project.id).to_a).to eq([developer_project, reporter_project]) - expect(subject.execute(project, search: nil, offset_id: developer_project.id).to_a).to eq([reporter_project]) - expect(subject.execute(project, search: nil, offset_id: reporter_project.id).to_a).to be_empty - end - end + expect(described_class.new(user, project_id: project.id, offset_id: maintainer_project.id).execute.to_a) + .to eq([developer_project, reporter_project]) - context 'search' do - it 'uses Project#search' do - expect(user).to receive_message_chain(:projects_where_can_admin_issues, :search) { Project.all } + expect(described_class.new(user, project_id: project.id, offset_id: developer_project.id).execute.to_a) + .to eq([reporter_project]) - subject.execute(project, search: 'wadus') + expect(described_class.new(user, project_id: project.id, offset_id: reporter_project.id).execute.to_a) + .to be_empty end + end + context 'search' do it 'returns projects matching a search query' do foo_project = create(:project) foo_project.add_maintainer(user) @@ -89,8 +98,11 @@ describe MoveToProjectFinder do wadus_project = create(:project, name: 'wadus') wadus_project.add_maintainer(user) - expect(subject.execute(project).to_a).to eq([wadus_project, foo_project]) - expect(subject.execute(project, search: 'wadus').to_a).to eq([wadus_project]) + expect(described_class.new(user, project_id: project.id).execute.to_a) + .to eq([wadus_project, foo_project]) + + expect(described_class.new(user, project_id: project.id, search: 'wadus').execute.to_a) + .to eq([wadus_project]) end end end diff --git a/spec/finders/autocomplete/project_finder_spec.rb b/spec/finders/autocomplete/project_finder_spec.rb new file mode 100644 index 00000000000..207d0598c28 --- /dev/null +++ b/spec/finders/autocomplete/project_finder_spec.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Autocomplete::ProjectFinder do + let(:user) { create(:user) } + + describe '#execute' do + context 'without a project ID' do + it 'returns nil' do + expect(described_class.new(user).execute).to be_nil + end + end + + context 'with an empty String as the project ID' do + it 'returns nil' do + expect(described_class.new(user, project_id: '').execute).to be_nil + end + end + + context 'with a project ID' do + it 'raises ActiveRecord::RecordNotFound if the project does not exist' do + finder = described_class.new(user, project_id: 1) + + expect { finder.execute }.to raise_error(ActiveRecord::RecordNotFound) + end + + it 'raises ActiveRecord::RecordNotFound if the user can not read the project' do + project = create(:project, :private) + + finder = described_class.new(user, project_id: project.id) + + expect { finder.execute }.to raise_error(ActiveRecord::RecordNotFound) + end + + it 'raises ActiveRecord::RecordNotFound if an anonymous user can not read the project' do + project = create(:project, :private) + + finder = described_class.new(nil, project_id: project.id) + + expect { finder.execute }.to raise_error(ActiveRecord::RecordNotFound) + end + + it 'returns the project if it exists and is readable' do + project = create(:project, :private) + + project.add_maintainer(user) + + finder = described_class.new(user, project_id: project.id) + + expect(finder.execute).to eq(project) + end + end + end +end diff --git a/spec/finders/autocomplete_users_finder_spec.rb b/spec/finders/autocomplete/users_finder_spec.rb index dcf9111776e..abd0d6b5185 100644 --- a/spec/finders/autocomplete_users_finder_spec.rb +++ b/spec/finders/autocomplete/users_finder_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe AutocompleteUsersFinder do +describe Autocomplete::UsersFinder do describe '#execute' do let!(:user1) { create(:user, username: 'johndoe') } let!(:user2) { create(:user, :blocked, username: 'notsorandom') } diff --git a/spec/finders/awarded_emoji_finder_spec.rb b/spec/finders/awarded_emoji_finder_spec.rb new file mode 100644 index 00000000000..d4479df7418 --- /dev/null +++ b/spec/finders/awarded_emoji_finder_spec.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe AwardedEmojiFinder do + describe '#execute' do + it 'returns an Array containing the awarded emoji names' do + user = create(:user) + + create(:award_emoji, user: user, name: 'thumbsup') + create(:award_emoji, user: user, name: 'thumbsup') + create(:award_emoji, user: user, name: 'thumbsdown') + + awarded = described_class.new(user).execute + + expect(awarded).to eq([{ name: 'thumbsup' }, { name: 'thumbsdown' }]) + end + + it 'returns an empty Array when no user is given' do + awarded = described_class.new.execute + + expect(awarded).to be_empty + end + end +end diff --git a/spec/finders/license_template_finder_spec.rb b/spec/finders/license_template_finder_spec.rb new file mode 100644 index 00000000000..a97903103c9 --- /dev/null +++ b/spec/finders/license_template_finder_spec.rb @@ -0,0 +1,49 @@ +require 'spec_helper' + +describe LicenseTemplateFinder do + describe '#execute' do + subject(:result) { described_class.new(params).execute } + + let(:categories) { categorised_licenses.keys } + let(:categorised_licenses) { result.group_by(&:category) } + + context 'popular: true' do + let(:params) { { popular: true } } + + it 'only returns popular licenses' do + expect(categories).to contain_exactly(:Popular) + expect(categorised_licenses[:Popular]).to be_present + end + end + + context 'popular: false' do + let(:params) { { popular: false } } + + it 'only returns unpopular licenses' do + expect(categories).to contain_exactly(:Other) + expect(categorised_licenses[:Other]).to be_present + end + end + + context 'popular: nil' do + let(:params) { { popular: nil } } + + it 'returns all licenses known by the Licensee gem' do + from_licensee = Licensee::License.all.map { |l| l.key } + + expect(result.map(&:id)).to match_array(from_licensee) + end + + it 'correctly copies all attributes' do + licensee = Licensee::License.all.first + found = result.find { |r| r.key == licensee.key } + + aggregate_failures do + %i[key name content nickname url meta featured?].each do |k| + expect(found.public_send(k)).to eq(licensee.public_send(k)) + end + end + end + end + end +end diff --git a/spec/finders/user_finder_spec.rb b/spec/finders/user_finder_spec.rb new file mode 100644 index 00000000000..e53aa50dd33 --- /dev/null +++ b/spec/finders/user_finder_spec.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe UserFinder do + describe '#execute' do + context 'when the user exists' do + it 'returns the user' do + user = create(:user) + found = described_class.new(id: user.id).execute + + expect(found).to eq(user) + end + end + + context 'when the user does not exist' do + it 'returns nil' do + found = described_class.new(id: 1).execute + + expect(found).to be_nil + end + end + end + + describe '#execute!' do + context 'when the user exists' do + it 'returns the user' do + user = create(:user) + found = described_class.new(id: user.id).execute! + + expect(found).to eq(user) + end + end + + context 'when the user does not exist' do + it 'raises ActiveRecord::RecordNotFound' do + finder = described_class.new(id: 1) + + expect { finder.execute! }.to raise_error(ActiveRecord::RecordNotFound) + end + end + end +end diff --git a/spec/helpers/avatars_helper_spec.rb b/spec/helpers/avatars_helper_spec.rb index 5856bccb5b8..55ee87163f9 100644 --- a/spec/helpers/avatars_helper_spec.rb +++ b/spec/helpers/avatars_helper_spec.rb @@ -5,12 +5,67 @@ describe AvatarsHelper do let(:user) { create(:user) } - describe '#project_icon' do - it 'returns an url for the avatar' do - project = create(:project, :public, avatar: File.open(uploaded_image_temp_path)) + describe '#project_icon & #group_icon' do + shared_examples 'resource with a default avatar' do |source_type| + it 'returns a default avatar div' do + expect(public_send("#{source_type}_icon", *helper_args)) + .to match(%r{<div class="identicon bg\d+">F</div>}) + end + end + + shared_examples 'resource with a custom avatar' do |source_type| + it 'returns a custom avatar image' do + expect(public_send("#{source_type}_icon", *helper_args)) + .to eq "<img src=\"#{resource.avatar.url}\" alt=\"Banana sample\" />" + end + end + + context 'when providing a project' do + it_behaves_like 'resource with a default avatar', 'project' do + let(:resource) { create(:project, name: 'foo') } + let(:helper_args) { [resource] } + end + + it_behaves_like 'resource with a custom avatar', 'project' do + let(:resource) { create(:project, :public, avatar: File.open(uploaded_image_temp_path)) } + let(:helper_args) { [resource] } + end + end + + context 'when providing a project path' do + it_behaves_like 'resource with a default avatar', 'project' do + let(:resource) { create(:project, name: 'foo') } + let(:helper_args) { [resource.full_path] } + end - expect(helper.project_icon(project.full_path).to_s) - .to eq "<img data-src=\"#{project.avatar.url}\" class=\" lazy\" src=\"#{LazyImageTagHelper.placeholder_image}\" />" + it_behaves_like 'resource with a custom avatar', 'project' do + let(:resource) { create(:project, :public, avatar: File.open(uploaded_image_temp_path)) } + let(:helper_args) { [resource.full_path] } + end + end + + context 'when providing a group' do + it_behaves_like 'resource with a default avatar', 'group' do + let(:resource) { create(:group, name: 'foo') } + let(:helper_args) { [resource] } + end + + it_behaves_like 'resource with a custom avatar', 'group' do + let(:resource) { create(:group, avatar: File.open(uploaded_image_temp_path)) } + let(:helper_args) { [resource] } + end + end + + context 'when providing a group path' do + it_behaves_like 'resource with a default avatar', 'group' do + let(:resource) { create(:group, name: 'foo') } + let(:helper_args) { [resource.full_path] } + end + + it_behaves_like 'resource with a custom avatar', 'group' do + let(:resource) { create(:group, avatar: File.open(uploaded_image_temp_path)) } + let(:helper_args) { [resource.full_path] } + end end end diff --git a/spec/helpers/button_helper_spec.rb b/spec/helpers/button_helper_spec.rb index 630f3eff258..0c0a0003231 100644 --- a/spec/helpers/button_helper_spec.rb +++ b/spec/helpers/button_helper_spec.rb @@ -79,6 +79,18 @@ describe ButtonHelper do end end + context 'without an ssh key on the user and user_show_add_ssh_key_message unset' do + before do + stub_application_setting(user_show_add_ssh_key_message: false) + end + + it 'there is no warning on the dropdown description' do + description = element.search('.dropdown-menu-inner-content').first + + expect(description).to be_nil + end + end + context 'with an ssh key on the user' do before do create(:key, user: user) diff --git a/spec/helpers/groups_helper_spec.rb b/spec/helpers/groups_helper_spec.rb index 115807f954b..540a8674ec2 100644 --- a/spec/helpers/groups_helper_spec.rb +++ b/spec/helpers/groups_helper_spec.rb @@ -3,19 +3,6 @@ require 'spec_helper' describe GroupsHelper do include ApplicationHelper - describe 'group_icon' do - it 'returns an url for the avatar' do - avatar_file_path = File.join('spec', 'fixtures', 'banana_sample.gif') - - group = create(:group) - group.avatar = fixture_file_upload(avatar_file_path) - group.save! - - expect(helper.group_icon(group).to_s) - .to eq "<img data-src=\"#{group.avatar.url}\" class=\" lazy\" src=\"#{LazyImageTagHelper.placeholder_image}\" />" - end - end - describe 'group_icon_url' do it 'returns an url for the avatar' do avatar_file_path = File.join('spec', 'fixtures', 'banana_sample.gif') diff --git a/spec/helpers/icons_helper_spec.rb b/spec/helpers/icons_helper_spec.rb index 82f588d1a08..4b40d523287 100644 --- a/spec/helpers/icons_helper_spec.rb +++ b/spec/helpers/icons_helper_spec.rb @@ -80,6 +80,26 @@ describe IconsHelper do end end + describe 'audit icon' do + it 'returns right icon name for standard auth' do + icon_name = 'standard' + expect(audit_icon(icon_name).to_s) + .to eq '<i class="fa fa-key"></i>' + end + + it 'returns right icon name for two-factor auth' do + icon_name = 'two-factor' + expect(audit_icon(icon_name).to_s) + .to eq '<i class="fa fa-key"></i>' + end + + it 'returns right icon name for google_oauth2 auth' do + icon_name = 'google_oauth2' + expect(audit_icon(icon_name).to_s) + .to eq '<i class="fa fa-google"></i>' + end + end + describe 'file_type_icon_class' do it 'returns folder class' do expect(file_type_icon_class('folder', 0, 'folder_name')).to eq 'folder' diff --git a/spec/javascripts/boards/board_list_spec.js b/spec/javascripts/boards/board_list_spec.js index de261d36c61..290600cf995 100644 --- a/spec/javascripts/boards/board_list_spec.js +++ b/spec/javascripts/boards/board_list_spec.js @@ -200,6 +200,15 @@ describe('Board list component', () => { }); }); + it('does not load issues if already loading', () => { + component.list.nextPage = spyOn(component.list, 'nextPage').and.returnValue(new Promise(() => {})); + + component.onScroll(); + component.onScroll(); + + expect(component.list.nextPage).toHaveBeenCalledTimes(1); + }); + it('shows loading more spinner', (done) => { component.showCount = true; component.list.loadingMore = true; diff --git a/spec/javascripts/diffs/components/diff_file_spec.js b/spec/javascripts/diffs/components/diff_file_spec.js index 7a4616ec8eb..44a38f7ca82 100644 --- a/spec/javascripts/diffs/components/diff_file_spec.js +++ b/spec/javascripts/diffs/components/diff_file_spec.js @@ -22,11 +22,18 @@ describe('DiffFile', () => { expect(el.id).toEqual(fileHash); expect(el.classList.contains('diff-file')).toEqual(true); + expect(el.querySelectorAll('.diff-content.hidden').length).toEqual(0); expect(el.querySelector('.js-file-title')).toBeDefined(); expect(el.querySelector('.file-title-name').innerText.indexOf(filePath) > -1).toEqual(true); expect(el.querySelector('.js-syntax-highlight')).toBeDefined(); - expect(el.querySelectorAll('.line_content').length > 5).toEqual(true); + + expect(vm.file.renderIt).toEqual(false); + vm.file.renderIt = true; + + vm.$nextTick(() => { + expect(el.querySelectorAll('.line_content').length > 5).toEqual(true); + }); }); describe('collapsed', () => { @@ -34,6 +41,7 @@ describe('DiffFile', () => { expect(vm.$el.querySelectorAll('.diff-content').length).toEqual(1); expect(vm.file.collapsed).toEqual(false); vm.file.collapsed = true; + vm.file.renderIt = true; vm.$nextTick(() => { expect(vm.$el.querySelectorAll('.diff-content').length).toEqual(0); diff --git a/spec/javascripts/diffs/mock_data/diff_file.js b/spec/javascripts/diffs/mock_data/diff_file.js index d3bf9525924..cce36ecc91f 100644 --- a/spec/javascripts/diffs/mock_data/diff_file.js +++ b/spec/javascripts/diffs/mock_data/diff_file.js @@ -39,6 +39,7 @@ export default { viewPath: '/gitlab-org/gitlab-test/blob/spooky-stuff/CHANGELOG', replacedViewPath: null, collapsed: false, + renderIt: false, tooLarge: false, contextLinesPath: '/gitlab-org/gitlab-test/blob/c48ee0d1bf3b30453f5b32250ce03134beaa6d13/CHANGELOG/diff', diff --git a/spec/javascripts/diffs/store/mutations_spec.js b/spec/javascripts/diffs/store/mutations_spec.js index 1af49f4985c..8f89984c6e5 100644 --- a/spec/javascripts/diffs/store/mutations_spec.js +++ b/spec/javascripts/diffs/store/mutations_spec.js @@ -1,6 +1,7 @@ import mutations from '~/diffs/store/mutations'; import * as types from '~/diffs/store/mutation_types'; import { INLINE_DIFF_VIEW_TYPE } from '~/diffs/constants'; +import diffFileMockData from '../mock_data/diff_file'; describe('DiffsStoreMutations', () => { describe('SET_BASE_CONFIG', () => { @@ -24,6 +25,23 @@ describe('DiffsStoreMutations', () => { }); }); + describe('SET_DIFF_DATA', () => { + it('should set diff data type properly', () => { + const state = {}; + const diffMock = { + diff_files: [diffFileMockData], + }; + + mutations[types.SET_DIFF_DATA](state, diffMock); + + const firstLine = state.diffFiles[0].parallelDiffLines[0]; + + expect(firstLine.right.text).toBeUndefined(); + expect(state.diffFiles[0].renderIt).toEqual(true); + expect(state.diffFiles[0].collapsed).toEqual(false); + }); + }); + describe('SET_DIFF_VIEW_TYPE', () => { it('should set diff view type properly', () => { const state = {}; diff --git a/spec/javascripts/ide/components/new_dropdown/button_spec.js b/spec/javascripts/ide/components/new_dropdown/button_spec.js index ef083d06ba7..6a326b5bd92 100644 --- a/spec/javascripts/ide/components/new_dropdown/button_spec.js +++ b/spec/javascripts/ide/components/new_dropdown/button_spec.js @@ -46,4 +46,20 @@ describe('IDE new entry dropdown button component', () => { done(); }); }); + + describe('tooltipTitle', () => { + it('returns empty string when showLabel is true', () => { + expect(vm.tooltipTitle).toBe(''); + }); + + it('returns label', done => { + vm.showLabel = false; + + vm.$nextTick(() => { + expect(vm.tooltipTitle).toBe('Testing'); + + done(); + }); + }); + }); }); diff --git a/spec/javascripts/ide/components/preview/clientside_spec.js b/spec/javascripts/ide/components/preview/clientside_spec.js index 3ec65882418..d6983f5a3b8 100644 --- a/spec/javascripts/ide/components/preview/clientside_spec.js +++ b/spec/javascripts/ide/components/preview/clientside_spec.js @@ -292,6 +292,8 @@ describe('IDE clientside preview', () => { describe('update', () => { beforeEach(() => { jasmine.clock().install(); + + vm.sandpackReady = true; vm.manager.updatePreview = jasmine.createSpy('updatePreview'); vm.manager.listener = jasmine.createSpy('updatePreview'); }); @@ -306,7 +308,7 @@ describe('IDE clientside preview', () => { vm.update(); - jasmine.clock().tick(500); + jasmine.clock().tick(250); expect(vm.initPreview).toHaveBeenCalled(); }); @@ -314,7 +316,7 @@ describe('IDE clientside preview', () => { it('calls updatePreview', () => { vm.update(); - jasmine.clock().tick(500); + jasmine.clock().tick(250); expect(vm.manager.updatePreview).toHaveBeenCalledWith(vm.sandboxOpts); }); diff --git a/spec/javascripts/ide/stores/actions/file_spec.js b/spec/javascripts/ide/stores/actions/file_spec.js index 72eb20bdc87..bca2033ff97 100644 --- a/spec/javascripts/ide/stores/actions/file_spec.js +++ b/spec/javascripts/ide/stores/actions/file_spec.js @@ -352,10 +352,22 @@ describe('IDE store file actions', () => { it('calls also getBaseRawFileData service method', done => { spyOn(service, 'getBaseRawFileData').and.returnValue(Promise.resolve('baseraw')); + store.state.currentProjectId = 'gitlab-org/gitlab-ce'; + store.state.currentMergeRequestId = '1'; + store.state.projects = { + 'gitlab-org/gitlab-ce': { + mergeRequests: { + 1: { + baseCommitSha: 'SHA', + }, + }, + }, + }; + tmpFile.mrChange = { new_file: false }; store - .dispatch('getRawFileData', { path: tmpFile.path, baseSha: 'SHA' }) + .dispatch('getRawFileData', { path: tmpFile.path }) .then(() => { expect(service.getBaseRawFileData).toHaveBeenCalledWith(tmpFile, 'SHA'); expect(tmpFile.baseRaw).toBe('baseraw'); @@ -392,10 +404,7 @@ describe('IDE store file actions', () => { const dispatch = jasmine.createSpy('dispatch'); actions - .getRawFileData( - { state: store.state, commit() {}, dispatch }, - { path: tmpFile.path, baseSha: tmpFile.baseSha }, - ) + .getRawFileData({ state: store.state, commit() {}, dispatch }, { path: tmpFile.path }) .then(done.fail) .catch(() => { expect(dispatch).toHaveBeenCalledWith('setErrorMessage', { @@ -404,7 +413,6 @@ describe('IDE store file actions', () => { actionText: 'Please try again', actionPayload: { path: tmpFile.path, - baseSha: tmpFile.baseSha, }, }); diff --git a/spec/javascripts/ide/stores/actions/merge_request_spec.js b/spec/javascripts/ide/stores/actions/merge_request_spec.js index 90c28c769f7..8564f04ce8a 100644 --- a/spec/javascripts/ide/stores/actions/merge_request_spec.js +++ b/spec/javascripts/ide/stores/actions/merge_request_spec.js @@ -1,12 +1,14 @@ import MockAdapter from 'axios-mock-adapter'; import axios from '~/lib/utils/axios_utils'; import store from '~/ide/stores'; -import { +import actions, { getMergeRequestData, getMergeRequestChanges, getMergeRequestVersions, + openMergeRequest, } from '~/ide/stores/actions/merge_request'; import service from '~/ide/services'; +import { activityBarViews } from '~/ide/constants'; import { resetStore } from '../../helpers'; describe('IDE store merge request actions', () => { @@ -238,4 +240,101 @@ describe('IDE store merge request actions', () => { }); }); }); + + describe('openMergeRequest', () => { + const mr = { + projectId: 'abcproject', + targetProjectId: 'defproject', + mergeRequestId: 2, + }; + let testMergeRequest; + let testMergeRequestChanges; + + beforeEach(() => { + testMergeRequest = { + source_branch: 'abcbranch', + }; + testMergeRequestChanges = { + changes: [], + }; + store.state.entries = { + foo: {}, + bar: {}, + }; + + spyOn(store, 'dispatch').and.callFake((type) => { + switch (type) { + case 'getMergeRequestData': + return Promise.resolve(testMergeRequest); + case 'getMergeRequestChanges': + return Promise.resolve(testMergeRequestChanges); + default: + return Promise.resolve(); + } + }); + }); + + it('dispatch actions for merge request data', done => { + openMergeRequest(store, mr) + .then(() => { + expect(store.dispatch.calls.allArgs()).toEqual([ + ['getMergeRequestData', mr], + ['setCurrentBranchId', testMergeRequest.source_branch], + ['getBranchData', { + projectId: mr.projectId, + branchId: testMergeRequest.source_branch, + }], + ['getFiles', { + projectId: mr.projectId, + branchId: testMergeRequest.source_branch, + }], + ['getMergeRequestVersions', mr], + ['getMergeRequestChanges', mr], + ]); + }) + .then(done) + .catch(done.fail); + }); + + it('updates activity bar view and gets file data, if changes are found', done => { + testMergeRequestChanges.changes = [ + { new_path: 'foo' }, + { new_path: 'bar' }, + ]; + + openMergeRequest(store, mr) + .then(() => { + expect(store.dispatch).toHaveBeenCalledWith('updateActivityBarView', activityBarViews.review); + + testMergeRequestChanges.changes.forEach((change, i) => { + expect(store.dispatch).toHaveBeenCalledWith('setFileMrChange', { + file: store.state.entries[change.new_path], + mrChange: change, + }); + expect(store.dispatch).toHaveBeenCalledWith('getFileData', { + path: change.new_path, + makeFileActive: i === 0, + }); + }); + }) + .then(done) + .catch(done.fail); + }); + + it('flashes message, if error', done => { + const flashSpy = spyOnDependency(actions, 'flash'); + store.dispatch.and.returnValue(Promise.reject()); + + openMergeRequest(store, mr) + .then(() => { + fail('Expected openMergeRequest to throw an error'); + }) + .catch(() => { + expect(flashSpy).toHaveBeenCalledWith(jasmine.any(String)); + }) + .then(done) + .catch(done.fail); + + }); + }); }); diff --git a/spec/javascripts/ide/stores/actions/project_spec.js b/spec/javascripts/ide/stores/actions/project_spec.js index 6a85968e199..667e3e0a7ef 100644 --- a/spec/javascripts/ide/stores/actions/project_spec.js +++ b/spec/javascripts/ide/stores/actions/project_spec.js @@ -5,6 +5,7 @@ import { showBranchNotFoundError, createNewBranchFromDefault, getBranchData, + openBranch, } from '~/ide/stores/actions'; import store from '~/ide/stores'; import service from '~/ide/services'; @@ -224,4 +225,55 @@ describe('IDE store project actions', () => { }); }); }); + + describe('openBranch', () => { + const branch = { + projectId: 'feature/lorem-ipsum', + branchId: '123-lorem', + }; + + beforeEach(() => { + store.state.entries = { + foo: { pending: false }, + 'foo/bar-pending': { pending: true }, + 'foo/bar': { pending: false }, + }; + + spyOn(store, 'dispatch').and.returnValue(Promise.resolve()); + }); + + it('dispatches branch actions', done => { + openBranch(store, branch) + .then(() => { + expect(store.dispatch.calls.allArgs()).toEqual([ + ['setCurrentBranchId', branch.branchId], + ['getBranchData', branch], + ['getFiles', branch], + ]); + }) + .then(done) + .catch(done.fail); + }); + + it('handles tree entry action, if basePath is given', done => { + openBranch(store, { ...branch, basePath: 'foo/bar/' }) + .then(() => { + expect(store.dispatch).toHaveBeenCalledWith( + 'handleTreeEntryAction', + store.state.entries['foo/bar'], + ); + }) + .then(done) + .catch(done.fail); + }); + + it('does not handle tree entry action, if entry is pending', done => { + openBranch(store, { ...branch, basePath: 'foo/bar-pending' }) + .then(() => { + expect(store.dispatch).not.toHaveBeenCalledWith('handleTreeEntryAction', jasmine.anything()); + }) + .then(done) + .catch(done.fail); + }); + }); }); diff --git a/spec/javascripts/ide/stores/modules/branches/actions_spec.js b/spec/javascripts/ide/stores/modules/branches/actions_spec.js index a0fce578958..010f56af03b 100644 --- a/spec/javascripts/ide/stores/modules/branches/actions_spec.js +++ b/spec/javascripts/ide/stores/modules/branches/actions_spec.js @@ -9,7 +9,6 @@ import { receiveBranchesSuccess, fetchBranches, resetBranches, - openBranch, } from '~/ide/stores/modules/branches/actions'; import { branches, projectData } from '../../../mock_data'; @@ -174,20 +173,5 @@ describe('IDE branches actions', () => { ); }); }); - - describe('openBranch', () => { - it('dispatches goToRoute action with path', done => { - const branchId = branches[0].name; - const expectedPath = `/project/${projectData.name_with_namespace}/edit/${branchId}`; - testAction( - openBranch, - branchId, - mockedState, - [], - [{ type: 'goToRoute', payload: expectedPath }], - done, - ); - }); - }); }); }); diff --git a/spec/javascripts/ide/stores/mutations_spec.js b/spec/javascripts/ide/stores/mutations_spec.js index 1e836dbc3f9..6ce76aaa03b 100644 --- a/spec/javascripts/ide/stores/mutations_spec.js +++ b/spec/javascripts/ide/stores/mutations_spec.js @@ -213,6 +213,33 @@ describe('Multi-file store mutations', () => { expect(localState.changedFiles).toEqual([localState.entries.filePath]); }); + + it('does not add tempFile into changedFiles', () => { + localState.entries.filePath = { + deleted: false, + type: 'blob', + tempFile: true, + }; + + mutations.DELETE_ENTRY(localState, 'filePath'); + + expect(localState.changedFiles).toEqual([]); + }); + + it('removes tempFile from changedFiles when deleted', () => { + localState.entries.filePath = { + path: 'filePath', + deleted: false, + type: 'blob', + tempFile: true, + }; + + localState.changedFiles.push({ ...localState.entries.filePath }); + + mutations.DELETE_ENTRY(localState, 'filePath'); + + expect(localState.changedFiles).toEqual([]); + }); }); describe('UPDATE_FILE_AFTER_COMMIT', () => { diff --git a/spec/javascripts/jobs/artifacts_block_spec.js b/spec/javascripts/jobs/artifacts_block_spec.js new file mode 100644 index 00000000000..c544c6f3e89 --- /dev/null +++ b/spec/javascripts/jobs/artifacts_block_spec.js @@ -0,0 +1,120 @@ +import Vue from 'vue'; +import { getTimeago } from '~/lib/utils/datetime_utility'; +import component from '~/jobs/components/artifacts_block.vue'; +import mountComponent from '../helpers/vue_mount_component_helper'; + +describe('Artifacts block', () => { + const Component = Vue.extend(component); + let vm; + + const expireAt = '2018-08-14T09:38:49.157Z'; + const timeago = getTimeago(); + const formatedDate = timeago.format(expireAt); + + afterEach(() => { + vm.$destroy(); + }); + + describe('with expired artifacts', () => { + it('renders expired artifact date and info', () => { + vm = mountComponent(Component, { + haveArtifactsExpired: true, + willArtifactsExpire: false, + expireAt, + }); + + expect(vm.$el.querySelector('.js-artifacts-removed')).not.toBeNull(); + expect(vm.$el.querySelector('.js-artifacts-will-be-removed')).toBeNull(); + expect(vm.$el.textContent).toContain(formatedDate); + }); + }); + + describe('with artifacts that will expire', () => { + it('renders will expire artifact date and info', () => { + vm = mountComponent(Component, { + haveArtifactsExpired: false, + willArtifactsExpire: true, + expireAt, + }); + + expect(vm.$el.querySelector('.js-artifacts-removed')).toBeNull(); + expect(vm.$el.querySelector('.js-artifacts-will-be-removed')).not.toBeNull(); + expect(vm.$el.textContent).toContain(formatedDate); + }); + }); + + describe('when the user can keep the artifacts', () => { + it('renders the keep button', () => { + vm = mountComponent(Component, { + haveArtifactsExpired: true, + willArtifactsExpire: false, + expireAt, + keepArtifactsPath: '/keep', + }); + + expect(vm.$el.querySelector('.js-keep-artifacts')).not.toBeNull(); + }); + }); + + describe('when the user can not keep the artifacts', () => { + it('does not render the keep button', () => { + vm = mountComponent(Component, { + haveArtifactsExpired: true, + willArtifactsExpire: false, + expireAt, + }); + + expect(vm.$el.querySelector('.js-keep-artifacts')).toBeNull(); + }); + }); + + describe('when the user can download the artifacts', () => { + it('renders the download button', () => { + vm = mountComponent(Component, { + haveArtifactsExpired: true, + willArtifactsExpire: false, + expireAt, + downloadArtifactsPath: '/download', + }); + + expect(vm.$el.querySelector('.js-download-artifacts')).not.toBeNull(); + }); + }); + + describe('when the user can not download the artifacts', () => { + it('does not render the keep button', () => { + vm = mountComponent(Component, { + haveArtifactsExpired: true, + willArtifactsExpire: false, + expireAt, + }); + + expect(vm.$el.querySelector('.js-download-artifacts')).toBeNull(); + }); + }); + + describe('when the user can browse the artifacts', () => { + it('does not render the browse button', () => { + vm = mountComponent(Component, { + haveArtifactsExpired: true, + willArtifactsExpire: false, + expireAt, + browseArtifactsPath: '/browse', + }); + + expect(vm.$el.querySelector('.js-browse-artifacts')).not.toBeNull(); + }); + }); + + describe('when the user can not browse the artifacts', () => { + it('does not render the browse button', () => { + vm = mountComponent(Component, { + haveArtifactsExpired: true, + willArtifactsExpire: false, + expireAt, + }); + + expect(vm.$el.querySelector('.js-browse-artifacts')).toBeNull(); + }); + }); +}); diff --git a/spec/javascripts/jobs/commit_block_spec.js b/spec/javascripts/jobs/commit_block_spec.js new file mode 100644 index 00000000000..f755a5042b5 --- /dev/null +++ b/spec/javascripts/jobs/commit_block_spec.js @@ -0,0 +1,73 @@ +import Vue from 'vue'; +import component from '~/jobs/components/commit_block.vue'; +import mountComponent from '../helpers/vue_mount_component_helper'; + +describe('Commit block', () => { + const Component = Vue.extend(component); + let vm; + + const props = { + pipelineShortSha: '1f0fb84f', + pipelineShaPath: 'commit/1f0fb84fb6770d74d97eee58118fd3909cd4f48c', + mergeRequestReference: '!21244', + mergeRequestPath: 'merge_requests/21244', + gitCommitTitlte: 'Regenerate pot files', + }; + + afterEach(() => { + vm.$destroy(); + }); + + describe('pipeline short sha', () => { + beforeEach(() => { + vm = mountComponent(Component, { + ...props, + }); + }); + + it('renders pipeline short sha link', () => { + expect(vm.$el.querySelector('.js-commit-sha').getAttribute('href')).toEqual(props.pipelineShaPath); + expect(vm.$el.querySelector('.js-commit-sha').textContent.trim()).toEqual(props.pipelineShortSha); + }); + + it('renders clipboard button', () => { + expect(vm.$el.querySelector('button').getAttribute('data-clipboard-text')).toEqual(props.pipelineShortSha); + }); + }); + + describe('with merge request', () => { + it('renders merge request link and reference', () => { + vm = mountComponent(Component, { + ...props, + }); + + expect(vm.$el.querySelector('.js-link-commit').getAttribute('href')).toEqual(props.mergeRequestPath); + expect(vm.$el.querySelector('.js-link-commit').textContent.trim()).toEqual(props.mergeRequestReference); + + }); + }); + + describe('without merge request', () => { + it('does not render merge request', () => { + const copyProps = Object.assign({}, props); + delete copyProps.mergeRequestPath; + delete copyProps.mergeRequestReference; + + vm = mountComponent(Component, { + ...copyProps, + }); + + expect(vm.$el.querySelector('.js-link-commit')).toBeNull(); + }); + }); + + describe('git commit title', () => { + it('renders git commit title', () => { + vm = mountComponent(Component, { + ...props, + }); + + expect(vm.$el.textContent).toContain(props.gitCommitTitlte); + }); + }); +}); diff --git a/spec/javascripts/jobs/components/job_log_controllers_spec.js b/spec/javascripts/jobs/components/job_log_controllers_spec.js new file mode 100644 index 00000000000..416dfab8a48 --- /dev/null +++ b/spec/javascripts/jobs/components/job_log_controllers_spec.js @@ -0,0 +1,217 @@ +import Vue from 'vue'; +import component from '~/jobs/components/job_log_controllers.vue'; +import mountComponent from '../../helpers/vue_mount_component_helper'; + +describe('Job log controllers', () => { + const Component = Vue.extend(component); + let vm; + + afterEach(() => { + vm.$destroy(); + }); + + describe('Truncate information', () => { + + beforeEach(() => { + vm = mountComponent(Component, { + rawTracePath: '/raw', + canEraseJob: true, + size: 511952, + canScrollToTop: true, + canScrollToBottom: true, + }); + }); + + it('renders size information', () => { + expect(vm.$el.querySelector('.js-truncated-info').textContent).toContain('499.95 KiB'); + }); + + it('renders link to raw trace', () => { + expect(vm.$el.querySelector('.js-raw-link').getAttribute('href')).toEqual('/raw'); + }); + + }); + + describe('links section', () => { + describe('with raw trace path', () => { + it('renders raw trace link', () => { + vm = mountComponent(Component, { + rawTracePath: '/raw', + canEraseJob: true, + size: 511952, + canScrollToTop: true, + canScrollToBottom: true, + }); + + expect(vm.$el.querySelector('.js-raw-link-controller').getAttribute('href')).toEqual('/raw'); + }); + }); + + describe('without raw trace path', () => { + it('does not render raw trace link', () => { + vm = mountComponent(Component, { + canEraseJob: true, + size: 511952, + canScrollToTop: true, + canScrollToBottom: true, + }); + + expect(vm.$el.querySelector('.js-raw-link-controller')).toBeNull(); + }); + }); + + describe('when is erasable', () => { + beforeEach(() => { + vm = mountComponent(Component, { + rawTracePath: '/raw', + canEraseJob: true, + size: 511952, + canScrollToTop: true, + canScrollToBottom: true, + }); + }); + + it('renders erase job button', () => { + expect(vm.$el.querySelector('.js-erase-link')).not.toBeNull(); + }); + + describe('on click', () => { + describe('when user confirms action', () => { + it('emits eraseJob event', () => { + spyOn(window, 'confirm').and.returnValue(true); + spyOn(vm, '$emit'); + + vm.$el.querySelector('.js-erase-link').click(); + + expect(vm.$emit).toHaveBeenCalledWith('eraseJob'); + }); + }); + + describe('when user does not confirm action', () => { + it('does not emit eraseJob event', () => { + spyOn(window, 'confirm').and.returnValue(false); + spyOn(vm, '$emit'); + + vm.$el.querySelector('.js-erase-link').click(); + + expect(vm.$emit).not.toHaveBeenCalledWith('eraseJob'); + }); + }); + }); + }); + + describe('when it is not erasable', () => { + it('does not render erase button', () => { + vm = mountComponent(Component, { + rawTracePath: '/raw', + canEraseJob: false, + size: 511952, + canScrollToTop: true, + canScrollToBottom: true, + }); + + expect(vm.$el.querySelector('.js-erase-link')).toBeNull(); + }); + }); + }); + + describe('scroll buttons', () => { + describe('scroll top button', () => { + describe('when user can scroll top', () => { + beforeEach(() => { + vm = mountComponent(Component, { + rawTracePath: '/raw', + canEraseJob: true, + size: 511952, + canScrollToTop: true, + canScrollToBottom: true, + }); + }); + + it('renders enabled scroll top button', () => { + expect(vm.$el.querySelector('.js-scroll-top').getAttribute('disabled')).toBeNull(); + }); + + it('emits scrollJobLogTop event on click', () => { + spyOn(vm, '$emit'); + vm.$el.querySelector('.js-scroll-top').click(); + + expect(vm.$emit).toHaveBeenCalledWith('scrollJobLogTop'); + }); + }); + + describe('when user can not scroll top', () => { + beforeEach(() => { + vm = mountComponent(Component, { + rawTracePath: '/raw', + canEraseJob: true, + size: 511952, + canScrollToTop: false, + canScrollToBottom: true, + }); + }); + + it('renders disabled scroll top button', () => { + expect(vm.$el.querySelector('.js-scroll-top').getAttribute('disabled')).toEqual('disabled'); + }); + + it('does not emit scrollJobLogTop event on click', () => { + spyOn(vm, '$emit'); + vm.$el.querySelector('.js-scroll-top').click(); + + expect(vm.$emit).not.toHaveBeenCalledWith('scrollJobLogTop'); + }); + }); + }); + + describe('scroll bottom button', () => { + describe('when user can scroll bottom', () => { + beforeEach(() => { + vm = mountComponent(Component, { + rawTracePath: '/raw', + canEraseJob: true, + size: 511952, + canScrollToTop: true, + canScrollToBottom: true, + }); + }); + + it('renders enabled scroll bottom button', () => { + expect(vm.$el.querySelector('.js-scroll-bottom').getAttribute('disabled')).toBeNull(); + }); + + it('emits scrollJobLogBottom event on click', () => { + spyOn(vm, '$emit'); + vm.$el.querySelector('.js-scroll-bottom').click(); + + expect(vm.$emit).toHaveBeenCalledWith('scrollJobLogBottom'); + }); + }); + + describe('when user can not scroll bottom', () => { + beforeEach(() => { + vm = mountComponent(Component, { + rawTracePath: '/raw', + canEraseJob: true, + size: 511952, + canScrollToTop: true, + canScrollToBottom: false, + }); + }); + + it('renders disabled scroll bottom button', () => { + expect(vm.$el.querySelector('.js-scroll-bottom').getAttribute('disabled')).toEqual('disabled'); + + }); + + it('does not emit scrollJobLogBottom event on click', () => { + spyOn(vm, '$emit'); + vm.$el.querySelector('.js-scroll-bottom').click(); + + expect(vm.$emit).not.toHaveBeenCalledWith('scrollJobLogBottom'); + }); + }); + }); + }); +}); + diff --git a/spec/javascripts/jobs/empty_state_spec.js b/spec/javascripts/jobs/empty_state_spec.js new file mode 100644 index 00000000000..f8feb069fe0 --- /dev/null +++ b/spec/javascripts/jobs/empty_state_spec.js @@ -0,0 +1,90 @@ +import Vue from 'vue'; +import component from '~/jobs/components/empty_state.vue'; +import mountComponent from '../helpers/vue_mount_component_helper'; + +describe('Empty State', () => { + const Component = Vue.extend(component); + let vm; + + const props = { + illustrationPath: 'illustrations/pending_job_empty.svg', + illustrationSizeClass: 'svg-430', + title: 'This job has not started yet', + }; + + const content = 'This job is in pending state and is waiting to be picked by a runner'; + + afterEach(() => { + vm.$destroy(); + }); + + describe('renders image and title', () => { + beforeEach(() => { + vm = mountComponent(Component, { + ...props, + content, + }); + }); + + it('renders img with provided path and size', () => { + expect(vm.$el.querySelector('img').getAttribute('src')).toEqual(props.illustrationPath); + expect(vm.$el.querySelector('.svg-content').classList).toContain(props.illustrationSizeClass); + }); + + it('renders provided title', () => { + expect(vm.$el.querySelector('.js-job-empty-state-title').textContent.trim()).toEqual( + props.title, + ); + }); + }); + + describe('with content', () => { + it('renders content', () => { + vm = mountComponent(Component, { + ...props, + content, + }); + + expect(vm.$el.querySelector('.js-job-empty-state-content').textContent.trim()).toEqual( + content, + ); + }); + }); + + describe('without content', () => { + it('does not render content', () => { + vm = mountComponent(Component, { + ...props, + }); + expect(vm.$el.querySelector('.js-job-empty-state-content')).toBeNull(); + }); + }); + + describe('with action', () => { + it('renders action', () => { + vm = mountComponent(Component, { + ...props, + content, + action: { + link: 'runner', + title: 'Check runner', + method: 'post', + }, + }); + + expect(vm.$el.querySelector('.js-job-empty-state-action').getAttribute('href')).toEqual( + 'runner', + ); + }); + }); + + describe('without action', () => { + it('does not render action', () => { + vm = mountComponent(Component, { + ...props, + content, + }); + expect(vm.$el.querySelector('.js-job-empty-state-action')).toBeNull(); + }); + }); +}); diff --git a/spec/javascripts/jobs/erased_block_spec.js b/spec/javascripts/jobs/erased_block_spec.js new file mode 100644 index 00000000000..7cf32e984a2 --- /dev/null +++ b/spec/javascripts/jobs/erased_block_spec.js @@ -0,0 +1,56 @@ +import Vue from 'vue'; +import { getTimeago } from '~/lib/utils/datetime_utility'; +import component from '~/jobs/components/erased_block.vue'; +import mountComponent from '../helpers/vue_mount_component_helper'; + +describe('Erased block', () => { + const Component = Vue.extend(component); + let vm; + + const erasedAt = '2016-11-07T11:11:16.525Z'; + const timeago = getTimeago(); + const formatedDate = timeago.format(erasedAt); + + afterEach(() => { + vm.$destroy(); + }); + + describe('with job erased by user', () => { + beforeEach(() => { + vm = mountComponent(Component, { + erasedByUser: true, + username: 'root', + linkToUser: 'gitlab.com/root', + erasedAt, + }); + }); + + it('renders username and link', () => { + expect(vm.$el.querySelector('a').getAttribute('href')).toEqual('gitlab.com/root'); + + expect(vm.$el.textContent).toContain('Job has been erased by'); + expect(vm.$el.textContent).toContain('root'); + }); + + it('renders erasedAt', () => { + expect(vm.$el.textContent).toContain(formatedDate); + }); + }); + + describe('with erased job', () => { + beforeEach(() => { + vm = mountComponent(Component, { + erasedByUser: false, + erasedAt, + }); + }); + + it('renders username and link', () => { + expect(vm.$el.textContent).toContain('Job has been erased'); + }); + + it('renders erasedAt', () => { + expect(vm.$el.textContent).toContain(formatedDate); + }); + }); +}); diff --git a/spec/javascripts/jobs/job_log_spec.js b/spec/javascripts/jobs/job_log_spec.js new file mode 100644 index 00000000000..406f1c4ccc5 --- /dev/null +++ b/spec/javascripts/jobs/job_log_spec.js @@ -0,0 +1,45 @@ +import Vue from 'vue'; +import component from '~/jobs/components/job_log.vue'; +import mountComponent from '../helpers/vue_mount_component_helper'; + +describe('Job Log', () => { + const Component = Vue.extend(component); + let vm; + + const trace = 'Running with gitlab-runner 11.1.0 (081978aa)<br> on docker-auto-scale-com d5ae8d25<br>Using Docker executor with image dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.4.4-golang-1.9-git-2.18-chrome-67.0-node-8.x-yarn-1.2-postgresql-9.6-graphicsmagick-1.3.29 ...<br>'; + + afterEach(() => { + vm.$destroy(); + }); + + it('renders provided trace', () => { + vm = mountComponent(Component, { + trace, + isReceivingBuildTrace: true, + }); + + expect(vm.$el.querySelector('code').textContent).toContain('Running with gitlab-runner 11.1.0 (081978aa)'); + }); + + describe('while receiving trace', () => { + it('renders animation', () => { + vm = mountComponent(Component, { + trace, + isReceivingBuildTrace: true, + }); + + expect(vm.$el.querySelector('.js-log-animation')).not.toBeNull(); + }); + }); + + describe('when build trace has finishes', () => { + it('does not render animation', () => { + vm = mountComponent(Component, { + trace, + isReceivingBuildTrace: false, + }); + + expect(vm.$el.querySelector('.js-log-animation')).toBeNull(); + }); + }); +}); diff --git a/spec/javascripts/jobs/jobs_container_spec.js b/spec/javascripts/jobs/jobs_container_spec.js new file mode 100644 index 00000000000..bf52e65cbc8 --- /dev/null +++ b/spec/javascripts/jobs/jobs_container_spec.js @@ -0,0 +1,126 @@ +import Vue from 'vue'; +import component from '~/jobs/components/jobs_container.vue'; +import mountComponent from '../helpers/vue_mount_component_helper'; + +describe('Artifacts block', () => { + const Component = Vue.extend(component); + let vm; + + const retried = { + status: { + details_path: '/gitlab-org/gitlab-ce/pipelines/28029444', + group: 'success', + has_details: true, + icon: 'status_success', + label: 'passed', + text: 'passed', + tooltip: 'passed', + }, + path: 'job/233432756', + id: '233432756', + tooltip: 'build - passed', + retried: true, + }; + + const active = { + name: 'test', + status: { + details_path: '/gitlab-org/gitlab-ce/pipelines/28029444', + group: 'success', + has_details: true, + icon: 'status_success', + label: 'passed', + text: 'passed', + tooltip: 'passed', + }, + path: 'job/2322756', + id: '2322756', + tooltip: 'build - passed', + active: true, + }; + + const job = { + name: 'build', + status: { + details_path: '/gitlab-org/gitlab-ce/pipelines/28029444', + group: 'success', + has_details: true, + icon: 'status_success', + label: 'passed', + text: 'passed', + tooltip: 'passed', + }, + path: 'job/232153', + id: '232153', + tooltip: 'build - passed', + }; + + afterEach(() => { + vm.$destroy(); + }); + + it('renders list of jobs', () => { + vm = mountComponent(Component, { + jobs: [job, retried, active], + }); + + expect(vm.$el.querySelectorAll('a').length).toEqual(3); + }); + + it('renders arrow right when job is active', () => { + vm = mountComponent(Component, { + jobs: [active], + }); + + expect(vm.$el.querySelector('a .js-arrow-right')).not.toBeNull(); + }); + + it('does not render arrow right when job is not active', () => { + vm = mountComponent(Component, { + jobs: [job], + }); + + expect(vm.$el.querySelector('a .js-arrow-right')).toBeNull(); + }); + + it('renders job name when present', () => { + vm = mountComponent(Component, { + jobs: [job], + }); + + expect(vm.$el.querySelector('a').textContent.trim()).toContain(job.name); + expect(vm.$el.querySelector('a').textContent.trim()).not.toContain(job.id); + }); + + it('renders job id when job name is not available', () => { + vm = mountComponent(Component, { + jobs: [retried], + }); + + expect(vm.$el.querySelector('a').textContent.trim()).toContain(retried.id); + }); + + it('links to the job page', () => { + vm = mountComponent(Component, { + jobs: [job], + }); + + expect(vm.$el.querySelector('a').getAttribute('href')).toEqual(job.path); + }); + + it('renders retry icon when job was retried', () => { + vm = mountComponent(Component, { + jobs: [retried], + }); + + expect(vm.$el.querySelector('.js-retry-icon')).not.toBeNull(); + }); + + it('does not render retry icon when job was not retried', () => { + vm = mountComponent(Component, { + jobs: [job], + }); + + expect(vm.$el.querySelector('.js-retry-icon')).toBeNull(); + }); +}); diff --git a/spec/javascripts/jobs/sidebar_details_block_spec.js b/spec/javascripts/jobs/sidebar_details_block_spec.js index 9c4454252ce..21ef5650b80 100644 --- a/spec/javascripts/jobs/sidebar_details_block_spec.js +++ b/spec/javascripts/jobs/sidebar_details_block_spec.js @@ -1,6 +1,7 @@ import Vue from 'vue'; import sidebarDetailsBlock from '~/jobs/components/sidebar_details_block.vue'; import job from './mock_data'; +import mountComponent from '../helpers/vue_mount_component_helper'; describe('Sidebar details block', () => { let SidebarComponent; @@ -20,39 +21,53 @@ describe('Sidebar details block', () => { describe('when it is loading', () => { it('should render a loading spinner', () => { - vm = new SidebarComponent({ - propsData: { - job: {}, - isLoading: true, - }, - }).$mount(); - + vm = mountComponent(SidebarComponent, { + job: {}, + isLoading: true, + }); expect(vm.$el.querySelector('.fa-spinner')).toBeDefined(); }); }); - describe("when user can't retry", () => { + describe('when there is no retry path retry', () => { it('should not render a retry button', () => { - vm = new SidebarComponent({ - propsData: { - job: {}, - canUserRetry: false, - isLoading: true, - }, - }).$mount(); + vm = mountComponent(SidebarComponent, { + job: {}, + isLoading: false, + }); expect(vm.$el.querySelector('.js-retry-job')).toBeNull(); }); }); - beforeEach(() => { - vm = new SidebarComponent({ - propsData: { + describe('without terminal path', () => { + it('does not render terminal link', () => { + vm = mountComponent(SidebarComponent, { job, - canUserRetry: true, isLoading: false, - }, - }).$mount(); + }); + + expect(vm.$el.querySelector('.js-terminal-link')).toBeNull(); + }); + }); + + describe('with terminal path', () => { + it('renders terminal link', () => { + vm = mountComponent(SidebarComponent, { + job, + isLoading: false, + terminalPath: 'job/43123/terminal', + }); + + expect(vm.$el.querySelector('.js-terminal-link')).not.toBeNull(); + }); + }); + + beforeEach(() => { + vm = mountComponent(SidebarComponent, { + job, + isLoading: false, + }); }); describe('actions', () => { @@ -102,13 +117,15 @@ describe('Sidebar details block', () => { }); it('should render runner ID', () => { - expect(trimWhitespace(vm.$el.querySelector('.js-job-runner'))).toEqual('Runner: local ci runner (#1)'); + expect(trimWhitespace(vm.$el.querySelector('.js-job-runner'))).toEqual( + 'Runner: local ci runner (#1)', + ); }); it('should render timeout information', () => { - expect( - trimWhitespace(vm.$el.querySelector('.js-job-timeout')), - ).toEqual('Timeout: 1m 40s (from runner)'); + expect(trimWhitespace(vm.$el.querySelector('.js-job-timeout'))).toEqual( + 'Timeout: 1m 40s (from runner)', + ); }); it('should render coverage', () => { diff --git a/spec/javascripts/jobs/stages_dropdown_spec.js b/spec/javascripts/jobs/stages_dropdown_spec.js new file mode 100644 index 00000000000..d3a5d48f56c --- /dev/null +++ b/spec/javascripts/jobs/stages_dropdown_spec.js @@ -0,0 +1,63 @@ +import Vue from 'vue'; +import component from '~/jobs/components/stages_dropdown.vue'; +import mountComponent from '../helpers/vue_mount_component_helper'; + +describe('Artifacts block', () => { + const Component = Vue.extend(component); + let vm; + + beforeEach(() => { + vm = mountComponent(Component, { + pipelineId: 28029444, + pipelinePath: 'pipeline/28029444', + pipelineRef: '50101-truncated-job-information', + pipelineRefPath: 'commits/50101-truncated-job-information', + stages: [ + { + name: 'build', + }, + { + name: 'test', + }, + ], + pipelineStatus: { + details_path: '/gitlab-org/gitlab-ce/pipelines/28029444', + group: 'success', + has_details: true, + icon: 'status_success', + label: 'passed', + text: 'passed', + tooltip: 'passed', + }, + }); + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('renders pipeline status', () => { + expect(vm.$el.querySelector('.js-ci-status-icon-success')).not.toBeNull(); + }); + + it('renders pipeline link', () => { + expect(vm.$el.querySelector('.js-pipeline-path').getAttribute('href')).toEqual( + 'pipeline/28029444', + ); + }); + + it('renders dropdown with stages', () => { + expect(vm.$el.querySelector('.dropdown button').textContent).toContain('build'); + }); + + it('updates selected stage on click', done => { + vm.$el.querySelectorAll('.stage-item')[1].click(); + vm + .$nextTick() + .then(() => { + expect(vm.$el.querySelector('.dropdown button').textContent).toContain('test'); + }) + .then(done) + .catch(done.fail); + }); +}); diff --git a/spec/javascripts/jobs/stuck_block_spec.js b/spec/javascripts/jobs/stuck_block_spec.js new file mode 100644 index 00000000000..4e2108dfdfb --- /dev/null +++ b/spec/javascripts/jobs/stuck_block_spec.js @@ -0,0 +1,81 @@ +import Vue from 'vue'; +import component from '~/jobs/components/stuck_block.vue'; +import mountComponent from '../helpers/vue_mount_component_helper'; + +describe('Stuck Block Job component', () => { + const Component = Vue.extend(component); + let vm; + + afterEach(() => { + vm.$destroy(); + }); + + describe('with no runners for project', () => { + beforeEach(() => { + vm = mountComponent(Component, { + hasNoRunnersForProject: true, + runnersPath: '/root/project/runners#js-runners-settings', + }); + }); + + it('renders only information about project not having runners', () => { + expect(vm.$el.querySelector('.js-stuck-no-runners')).not.toBeNull(); + expect(vm.$el.querySelector('.js-stuck-with-tags')).toBeNull(); + expect(vm.$el.querySelector('.js-stuck-no-active-runner')).toBeNull(); + }); + + it('renders link to runners page', () => { + expect(vm.$el.querySelector('.js-runners-path').getAttribute('href')).toEqual( + '/root/project/runners#js-runners-settings', + ); + }); + }); + + describe('with tags', () => { + beforeEach(() => { + vm = mountComponent(Component, { + hasNoRunnersForProject: false, + tags: ['docker', 'gitlab-org'], + runnersPath: '/root/project/runners#js-runners-settings', + }); + }); + + it('renders information about the tags not being set', () => { + expect(vm.$el.querySelector('.js-stuck-no-runners')).toBeNull(); + expect(vm.$el.querySelector('.js-stuck-with-tags')).not.toBeNull(); + expect(vm.$el.querySelector('.js-stuck-no-active-runner')).toBeNull(); + }); + + it('renders tags', () => { + expect(vm.$el.textContent).toContain('docker'); + expect(vm.$el.textContent).toContain('gitlab-org'); + }); + + it('renders link to runners page', () => { + expect(vm.$el.querySelector('.js-runners-path').getAttribute('href')).toEqual( + '/root/project/runners#js-runners-settings', + ); + }); + }); + + describe('without active runners', () => { + beforeEach(() => { + vm = mountComponent(Component, { + hasNoRunnersForProject: false, + runnersPath: '/root/project/runners#js-runners-settings', + }); + }); + + it('renders information about project not having runners', () => { + expect(vm.$el.querySelector('.js-stuck-no-runners')).toBeNull(); + expect(vm.$el.querySelector('.js-stuck-with-tags')).toBeNull(); + expect(vm.$el.querySelector('.js-stuck-no-active-runner')).not.toBeNull(); + }); + + it('renders link to runners page', () => { + expect(vm.$el.querySelector('.js-runners-path').getAttribute('href')).toEqual( + '/root/project/runners#js-runners-settings', + ); + }); + }); +}); diff --git a/spec/javascripts/jobs/trigger_value_spec.js b/spec/javascripts/jobs/trigger_value_spec.js new file mode 100644 index 00000000000..acf91510ed2 --- /dev/null +++ b/spec/javascripts/jobs/trigger_value_spec.js @@ -0,0 +1,66 @@ +import Vue from 'vue'; +import component from '~/jobs/components/trigger_block.vue'; +import mountComponent from '../helpers/vue_mount_component_helper'; + +describe('Trigger block', () => { + const Component = Vue.extend(component); + let vm; + + afterEach(() => { + vm.$destroy(); + }); + + describe('with short token', () => { + it('renders short token', () => { + vm = mountComponent(Component, { + shortToken: '0a666b2', + }); + + expect(vm.$el.querySelector('.js-short-token').textContent).toContain('0a666b2'); + }); + }); + + describe('without short token', () => { + it('does not render short token', () => { + vm = mountComponent(Component, {}); + + expect(vm.$el.querySelector('.js-short-token')).toBeNull(); + }); + }); + + describe('with variables', () => { + describe('reveal variables', () => { + it('reveals variables on click', done => { + vm = mountComponent(Component, { + variables: { + key: 'value', + variable: 'foo', + }, + }); + + vm.$el.querySelector('.js-reveal-variables').click(); + + vm + .$nextTick() + .then(() => { + expect(vm.$el.querySelector('.js-build-variables')).not.toBeNull(); + expect(vm.$el.querySelector('.js-build-variables').textContent).toContain('key'); + expect(vm.$el.querySelector('.js-build-variables').textContent).toContain('value'); + expect(vm.$el.querySelector('.js-build-variables').textContent).toContain('variable'); + expect(vm.$el.querySelector('.js-build-variables').textContent).toContain('foo'); + }) + .then(done) + .catch(done.fail); + }); + }); + }); + + describe('without variables', () => { + it('does not render variables', () => { + vm = mountComponent(Component); + + expect(vm.$el.querySelector('.js-reveal-variables')).toBeNull(); + expect(vm.$el.querySelector('.js-build-variables')).toBeNull(); + }); + }); +}); diff --git a/spec/javascripts/locale/ensure_single_line_spec.js b/spec/javascripts/locale/ensure_single_line_spec.js new file mode 100644 index 00000000000..bbefa8f40f3 --- /dev/null +++ b/spec/javascripts/locale/ensure_single_line_spec.js @@ -0,0 +1,35 @@ +import ensureSingleLine from '~/locale/ensure_single_line'; + +describe('locale', () => { + describe('ensureSingleLine', () => { + it('should remove newlines at the start of the string', () => { + const result = 'Test'; + expect(ensureSingleLine(`\n${result}`)).toBe(result); + expect(ensureSingleLine(`\t\n\t${result}`)).toBe(result); + expect(ensureSingleLine(`\r\n${result}`)).toBe(result); + expect(ensureSingleLine(`\r\n ${result}`)).toBe(result); + expect(ensureSingleLine(`\r ${result}`)).toBe(result); + expect(ensureSingleLine(` \n ${result}`)).toBe(result); + }); + + it('should remove newlines at the end of the string', () => { + const result = 'Test'; + expect(ensureSingleLine(`${result}\n`)).toBe(result); + expect(ensureSingleLine(`${result}\t\n\t`)).toBe(result); + expect(ensureSingleLine(`${result}\r\n`)).toBe(result); + expect(ensureSingleLine(`${result}\r`)).toBe(result); + expect(ensureSingleLine(`${result} \r`)).toBe(result); + expect(ensureSingleLine(`${result} \r\n `)).toBe(result); + }); + + it('should replace newlines in the middle of the string with a single space', () => { + const result = 'Test'; + expect(ensureSingleLine(`${result}\n${result}`)).toBe(`${result} ${result}`); + expect(ensureSingleLine(`${result}\t\n\t${result}`)).toBe(`${result} ${result}`); + expect(ensureSingleLine(`${result}\r\n${result}`)).toBe(`${result} ${result}`); + expect(ensureSingleLine(`${result}\r${result}`)).toBe(`${result} ${result}`); + expect(ensureSingleLine(`${result} \r${result}`)).toBe(`${result} ${result}`); + expect(ensureSingleLine(`${result} \r\n ${result}`)).toBe(`${result} ${result}`); + }); + }); +}); diff --git a/spec/javascripts/notes/mock_data.js b/spec/javascripts/notes/mock_data.js index 67f6a9629d9..0423fcb6ec4 100644 --- a/spec/javascripts/notes/mock_data.js +++ b/spec/javascripts/notes/mock_data.js @@ -1104,7 +1104,7 @@ export const collapsedSystemNotes = [ resolvable: false, noteable_iid: 12, note: 'changed the description', - note_html: '\n <p dir="auto">changed the description 2 times within 1 minute </p>', + note_html: ' <p dir="auto">changed the description 2 times within 1 minute </p>', current_user: { can_edit: false, can_award_emoji: true }, resolved: false, resolved_by: null, diff --git a/spec/javascripts/vue_shared/components/reports/modal_open_name_spec.js b/spec/javascripts/reports/components/modal_open_name_spec.js index 8635203c413..b18b3ef03d1 100644 --- a/spec/javascripts/vue_shared/components/reports/modal_open_name_spec.js +++ b/spec/javascripts/reports/components/modal_open_name_spec.js @@ -1,6 +1,6 @@ import Vue from 'vue'; import Vuex from 'vuex'; -import component from '~/vue_shared/components/reports/modal_open_name.vue'; +import component from '~/reports/components/modal_open_name.vue'; import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; describe('Modal open name', () => { diff --git a/spec/javascripts/vue_shared/components/reports/report_link_spec.js b/spec/javascripts/reports/components/report_link_spec.js index a4691f3712f..cd6911e2f59 100644 --- a/spec/javascripts/vue_shared/components/reports/report_link_spec.js +++ b/spec/javascripts/reports/components/report_link_spec.js @@ -1,6 +1,6 @@ import Vue from 'vue'; -import component from '~/vue_shared/components/reports/report_link.vue'; -import mountComponent from '../../../helpers/vue_mount_component_helper'; +import component from '~/reports/components/report_link.vue'; +import mountComponent from '../../helpers/vue_mount_component_helper'; describe('report link', () => { let vm; diff --git a/spec/javascripts/vue_shared/components/reports/report_section_spec.js b/spec/javascripts/reports/components/report_section_spec.js index 4e3986acb16..6f6eb161d14 100644 --- a/spec/javascripts/vue_shared/components/reports/report_section_spec.js +++ b/spec/javascripts/reports/components/report_section_spec.js @@ -1,5 +1,5 @@ import Vue from 'vue'; -import reportSection from '~/vue_shared/components/reports/report_section.vue'; +import reportSection from '~/reports/components/report_section.vue'; import mountComponent, { mountComponentWithSlots } from 'spec/helpers/vue_mount_component_helper'; describe('Report section', () => { diff --git a/spec/javascripts/vue_shared/components/reports/summary_row_spec.js b/spec/javascripts/reports/components/summary_row_spec.js index ac076f05bc0..fab7693581c 100644 --- a/spec/javascripts/vue_shared/components/reports/summary_row_spec.js +++ b/spec/javascripts/reports/components/summary_row_spec.js @@ -1,5 +1,5 @@ import Vue from 'vue'; -import component from '~/vue_shared/components/reports/summary_row.vue'; +import component from '~/reports/components/summary_row.vue'; import mountComponent from 'spec/helpers/vue_mount_component_helper'; describe('Summary row', () => { diff --git a/spec/javascripts/vue_shared/translate_spec.js b/spec/javascripts/vue_shared/translate_spec.js index cbb3cbdff46..adb5ff682f0 100644 --- a/spec/javascripts/vue_shared/translate_spec.js +++ b/spec/javascripts/vue_shared/translate_spec.js @@ -1,88 +1,249 @@ import Vue from 'vue'; -import Translate from '~/vue_shared/translate'; +import Jed from 'jed'; -Vue.use(Translate); +import locale from '~/locale'; +import Translate from '~/vue_shared/translate'; +import { trimText } from 'spec/helpers/vue_component_helper'; describe('Vue translate filter', () => { let el; + const createTranslationMock = (key, ...translations) => { + const fakeLocale = new Jed({ + domain: 'app', + locale_data: { + app: { + '': { + domain: 'app', + lang: 'vo', + plural_forms: 'nplurals=2; plural=(n != 1);', + }, + [key]: translations, + }, + }, + }); + + // eslint-disable-next-line no-underscore-dangle + locale.__Rewire__('locale', fakeLocale); + }; + + afterEach(() => { + // eslint-disable-next-line no-underscore-dangle + locale.__ResetDependency__('locale'); + }); + beforeEach(() => { + Vue.use(Translate); + el = document.createElement('div'); document.body.appendChild(el); }); - it('translate single text', (done) => { - const comp = new Vue({ + it('translate singular text (`__`)', done => { + const key = 'singular'; + const translation = 'singular_translated'; + createTranslationMock(key, translation); + + const vm = new Vue({ + el, + template: ` + <span> + {{ __('${key}') }} + </span> + `, + }).$mount(); + + Vue.nextTick(() => { + expect(trimText(vm.$el.textContent)).toBe(translation); + + done(); + }); + }); + + it('translate plural text (`n__`) without any substituting text', done => { + const key = 'plural'; + const translationPlural = 'plural_multiple translation'; + createTranslationMock(key, 'plural_singular translation', translationPlural); + + const vm = new Vue({ el, template: ` <span> - {{ __('testing') }} + {{ n__('${key}', 'plurals', 2) }} </span> `, }).$mount(); Vue.nextTick(() => { - expect( - comp.$el.textContent.trim(), - ).toBe('testing'); + expect(trimText(vm.$el.textContent)).toBe(translationPlural); done(); }); }); - it('translate plural text with single count', (done) => { - const comp = new Vue({ + describe('translate plural text (`n__`) with substituting %d', () => { + const key = '%d day'; + + beforeEach(() => { + createTranslationMock(key, '%d singular translated', '%d plural translated'); + }); + + it('and n === 1', done => { + const vm = new Vue({ + el, + template: ` + <span> + {{ n__('${key}', '%d days', 1) }} + </span> + `, + }).$mount(); + + Vue.nextTick(() => { + expect(trimText(vm.$el.textContent)).toBe('1 singular translated'); + + done(); + }); + }); + + it('and n > 1', done => { + const vm = new Vue({ + el, + template: ` + <span> + {{ n__('${key}', '%d days', 2) }} + </span> + `, + }).$mount(); + + Vue.nextTick(() => { + expect(trimText(vm.$el.textContent)).toBe('2 plural translated'); + + done(); + }); + }); + }); + + describe('translates text with context `s__`', () => { + const key = 'Context|Foobar'; + const translation = 'Context|Foobar translated'; + const expectation = 'Foobar translated'; + + beforeEach(() => { + createTranslationMock(key, translation); + }); + + it('and using two parameters', done => { + const vm = new Vue({ + el, + template: ` + <span> + {{ s__('Context', 'Foobar') }} + </span> + `, + }).$mount(); + + Vue.nextTick(() => { + expect(trimText(vm.$el.textContent)).toBe(expectation); + + done(); + }); + }); + + it('and using the pipe syntax', done => { + const vm = new Vue({ + el, + template: ` + <span> + {{ s__('${key}') }} + </span> + `, + }).$mount(); + + Vue.nextTick(() => { + expect(trimText(vm.$el.textContent)).toBe(expectation); + + done(); + }); + }); + }); + + it('translate multi line text', done => { + const translation = 'multiline string translated'; + createTranslationMock('multiline string', translation); + + const vm = new Vue({ el, template: ` <span> - {{ n__('%d day', '%d days', 1) }} + {{ __(\` + multiline + string + \`) }} </span> `, }).$mount(); Vue.nextTick(() => { - expect( - comp.$el.textContent.trim(), - ).toBe('1 day'); + expect(trimText(vm.$el.textContent)).toBe(translation); done(); }); }); - it('translate plural text with multiple count', (done) => { - const comp = new Vue({ + it('translate pluralized multi line text', done => { + const translation = 'multiline string plural'; + + createTranslationMock('multiline string', 'multiline string singular', translation); + + const vm = new Vue({ el, template: ` <span> - {{ n__('%d day', '%d days', 2) }} + {{ n__( + \` + multiline + string + \`, + \` + multiline + strings + \`, + 2 + ) }} </span> `, }).$mount(); Vue.nextTick(() => { - expect( - comp.$el.textContent.trim(), - ).toBe('2 days'); + expect(trimText(vm.$el.textContent)).toBe(translation); done(); }); }); - it('translate plural without replacing any text', (done) => { - const comp = new Vue({ + it('translate pluralized multi line text with context', done => { + const translation = 'multiline string with context'; + + createTranslationMock('Context| multiline string', translation); + + const vm = new Vue({ el, template: ` <span> - {{ n__('day', 'days', 2) }} + {{ s__( + \` + Context| + multiline + string + \` + ) }} </span> `, }).$mount(); Vue.nextTick(() => { - expect( - comp.$el.textContent.trim(), - ).toBe('days'); + expect(trimText(vm.$el.textContent)).toBe(translation); done(); }); diff --git a/spec/lib/banzai/filter/project_reference_filter_spec.rb b/spec/lib/banzai/filter/project_reference_filter_spec.rb new file mode 100644 index 00000000000..48140305e26 --- /dev/null +++ b/spec/lib/banzai/filter/project_reference_filter_spec.rb @@ -0,0 +1,85 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Banzai::Filter::ProjectReferenceFilter do + include FilterSpecHelper + + def invalidate_reference(reference) + "#{reference.reverse}" + end + + def get_reference(project) + project.to_reference_with_postfix + end + + let(:project) { create(:project, :public) } + subject { project } + let(:subject_name) { "project" } + let(:reference) { get_reference(project) } + + it_behaves_like 'user reference or project reference' + + it 'ignores invalid projects' do + exp = act = "Hey #{invalidate_reference(reference)}" + + expect(reference_filter(act).to_html).to eq(CGI.escapeHTML(exp)) + end + + it 'allows references with text after the > character' do + doc = reference_filter("Hey #{reference}foo") + expect(doc.css('a').first.attr('href')).to eq urls.project_url(subject) + end + + %w(pre code a style).each do |elem| + it "ignores valid references contained inside '#{elem}' element" do + exp = act = "<#{elem}>Hey #{CGI.escapeHTML(reference)}</#{elem}>" + expect(reference_filter(act).to_html).to eq exp + end + end + + it 'includes default classes' do + doc = reference_filter("Hey #{reference}") + expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-project has-tooltip' + end + + context 'in group context' do + let(:group) { create(:group) } + let(:project) { create(:project, group: group) } + + let(:nested_group) { create(:group, :nested) } + let(:nested_project) { create(:project, group: nested_group) } + + it 'supports mentioning a project' do + reference = get_reference(project) + doc = reference_filter("Hey #{reference}") + + expect(doc.css('a').first.attr('href')).to eq urls.project_url(project) + end + + it 'supports mentioning a project in a nested group' do + reference = get_reference(nested_project) + doc = reference_filter("Hey #{reference}") + + expect(doc.css('a').first.attr('href')).to eq urls.project_url(nested_project) + end + end + + describe '#projects_hash' do + it 'returns a Hash containing all Projects' do + document = Nokogiri::HTML.fragment("<p>#{get_reference(project)}</p>") + filter = described_class.new(document, project: project) + + expect(filter.projects_hash).to eq({ project.full_path => project }) + end + end + + describe '#projects' do + it 'returns the projects mentioned in a document' do + document = Nokogiri::HTML.fragment("<p>#{get_reference(project)}</p>") + filter = described_class.new(document, project: project) + + expect(filter.projects).to eq([project.full_path]) + end + end +end diff --git a/spec/lib/banzai/filter/user_reference_filter_spec.rb b/spec/lib/banzai/filter/user_reference_filter_spec.rb index 2f86a046d28..334d29a5368 100644 --- a/spec/lib/banzai/filter/user_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/user_reference_filter_spec.rb @@ -3,9 +3,17 @@ require 'spec_helper' describe Banzai::Filter::UserReferenceFilter do include FilterSpecHelper + def get_reference(user) + user.to_reference + end + let(:project) { create(:project, :public) } let(:user) { create(:user) } - let(:reference) { user.to_reference } + subject { user } + let(:subject_name) { "user" } + let(:reference) { get_reference(user) } + + it_behaves_like 'user reference or project reference' it 'requires project context' do expect { described_class.call('') }.to raise_error(ArgumentError, /:project/) @@ -66,45 +74,6 @@ describe Banzai::Filter::UserReferenceFilter do end end - context 'mentioning a user' do - it_behaves_like 'a reference containing an element node' - - it 'links to a User' do - doc = reference_filter("Hey #{reference}") - expect(doc.css('a').first.attr('href')).to eq urls.user_url(user) - end - - it 'links to a User with a period' do - user = create(:user, name: 'alphA.Beta') - - doc = reference_filter("Hey #{user.to_reference}") - expect(doc.css('a').length).to eq 1 - end - - it 'links to a User with an underscore' do - user = create(:user, name: 'ping_pong_king') - - doc = reference_filter("Hey #{user.to_reference}") - expect(doc.css('a').length).to eq 1 - end - - it 'links to a User with different case-sensitivity' do - user = create(:user, username: 'RescueRanger') - - doc = reference_filter("Hey #{user.to_reference.upcase}") - expect(doc.css('a').length).to eq 1 - expect(doc.css('a').text).to eq(user.to_reference) - end - - it 'includes a data-user attribute' do - doc = reference_filter("Hey #{reference}") - link = doc.css('a').first - - expect(link).to have_attribute('data-user') - expect(link.attr('data-user')).to eq user.namespace.owner_id.to_s - end - end - context 'mentioning a group' do it_behaves_like 'a reference containing an element node' @@ -154,36 +123,6 @@ describe Banzai::Filter::UserReferenceFilter do expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-project_member has-tooltip' end - it 'supports an :only_path context' do - doc = reference_filter("Hey #{reference}", only_path: true) - link = doc.css('a').first.attr('href') - - expect(link).not_to match %r(https?://) - expect(link).to eq urls.user_path(user) - end - - context 'referencing a user in a link href' do - let(:reference) { %Q{<a href="#{user.to_reference}">User</a>} } - - it 'links to a User' do - doc = reference_filter("Hey #{reference}") - expect(doc.css('a').first.attr('href')).to eq urls.user_url(user) - end - - it 'links with adjacent text' do - doc = reference_filter("Mention me (#{reference}.)") - expect(doc.to_html).to match(%r{\(<a.+>User</a>\.\)}) - end - - it 'includes a data-user attribute' do - doc = reference_filter("Hey #{reference}") - link = doc.css('a').first - - expect(link).to have_attribute('data-user') - expect(link.attr('data-user')).to eq user.namespace.owner_id.to_s - end - end - context 'when a project is not specified' do let(:project) { nil } @@ -227,7 +166,7 @@ describe Banzai::Filter::UserReferenceFilter do end it 'supports mentioning a single user' do - reference = group_member.to_reference + reference = get_reference(group_member) doc = reference_filter("Hey #{reference}", context) expect(doc.css('a').first.attr('href')).to eq urls.user_url(group_member) @@ -243,7 +182,7 @@ describe Banzai::Filter::UserReferenceFilter do describe '#namespaces' do it 'returns a Hash containing all Namespaces' do - document = Nokogiri::HTML.fragment("<p>#{user.to_reference}</p>") + document = Nokogiri::HTML.fragment("<p>#{get_reference(user)}</p>") filter = described_class.new(document, project: project) ns = user.namespace @@ -253,7 +192,7 @@ describe Banzai::Filter::UserReferenceFilter do describe '#usernames' do it 'returns the usernames mentioned in a document' do - document = Nokogiri::HTML.fragment("<p>#{user.to_reference}</p>") + document = Nokogiri::HTML.fragment("<p>#{get_reference(user)}</p>") filter = described_class.new(document, project: project) expect(filter.usernames).to eq([user.username]) diff --git a/spec/lib/banzai/reference_parser/project_parser_spec.rb b/spec/lib/banzai/reference_parser/project_parser_spec.rb new file mode 100644 index 00000000000..e4936aa9e57 --- /dev/null +++ b/spec/lib/banzai/reference_parser/project_parser_spec.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Banzai::ReferenceParser::ProjectParser do + include ReferenceParserHelpers + + let(:project) { create(:project, :public) } + let(:user) { create(:user) } + subject { described_class.new(Banzai::RenderContext.new(project, user)) } + let(:link) { empty_html_link } + + describe '#referenced_by' do + describe 'when the link has a data-project attribute' do + context 'using an existing project ID' do + it 'returns an Array of projects' do + link['data-project'] = project.id.to_s + + expect(subject.gather_references([link])).to eq([project]) + end + end + + context 'using a non-existing project ID' do + it 'returns an empty Array' do + link['data-project'] = '' + + expect(subject.gather_references([link])).to eq([]) + end + end + + context 'using a private project ID' do + it 'returns an empty Array when unauthorized' do + private_project = create(:project, :private) + + link['data-project'] = private_project.id.to_s + + expect(subject.gather_references([link])).to eq([]) + end + + it 'returns an Array when authorized' do + private_project = create(:project, :private, namespace: user.namespace) + + link['data-project'] = private_project.id.to_s + + expect(subject.gather_references([link])).to eq([private_project]) + end + end + end + end +end 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 26d48cc8201..f92acf61682 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 @@ -5,7 +5,7 @@ describe Gitlab::BackgroundMigration::CreateGpgKeySubkeysFromGpgKeys, :migration let!(:gpg_key) { create(:gpg_key, key: GpgHelpers::User3.public_key) } before do - GpgKeySubkey.destroy_all + GpgKeySubkey.destroy_all # rubocop: disable DestroyAll end it 'generate the subkeys' do diff --git a/spec/lib/gitlab/bare_repository_import/importer_spec.rb b/spec/lib/gitlab/bare_repository_import/importer_spec.rb index 6e21c846c0a..3c63e601abc 100644 --- a/spec/lib/gitlab/bare_repository_import/importer_spec.rb +++ b/spec/lib/gitlab/bare_repository_import/importer_spec.rb @@ -10,9 +10,6 @@ describe Gitlab::BareRepositoryImport::Importer, :seed_helper do subject(:importer) { described_class.new(admin, bare_repository) } before do - @rainbow = Rainbow.enabled - Rainbow.enabled = false - allow(described_class).to receive(:log) end @@ -20,7 +17,6 @@ describe Gitlab::BareRepositoryImport::Importer, :seed_helper do FileUtils.rm_rf(base_dir) TestEnv.clean_test_path ensure_seeds - Rainbow.enabled = @rainbow end shared_examples 'importing a repository' do diff --git a/spec/lib/gitlab/cleanup/project_uploads_spec.rb b/spec/lib/gitlab/cleanup/project_uploads_spec.rb index 37b38776775..11e605eece6 100644 --- a/spec/lib/gitlab/cleanup/project_uploads_spec.rb +++ b/spec/lib/gitlab/cleanup/project_uploads_spec.rb @@ -244,9 +244,11 @@ describe Gitlab::Cleanup::ProjectUploads do orphaned1 = create(:upload, :personal_snippet_upload, :with_file) orphaned2 = create(:upload, :namespace_upload, :with_file) orphaned3 = create(:upload, :attachment_upload, :with_file) + orphaned4 = create(:upload, :favicon_upload, :with_file) paths << orphaned1.absolute_path paths << orphaned2.absolute_path paths << orphaned3.absolute_path + paths << orphaned4.absolute_path Upload.delete_all expect(logger).not_to receive(:info).with(/move|fix/i) diff --git a/spec/lib/gitlab/data_builder/build_spec.rb b/spec/lib/gitlab/data_builder/build_spec.rb index ee91decafad..14fe196a986 100644 --- a/spec/lib/gitlab/data_builder/build_spec.rb +++ b/spec/lib/gitlab/data_builder/build_spec.rb @@ -15,6 +15,7 @@ describe Gitlab::DataBuilder::Build do it { expect(data[:build_id]).to eq(build.id) } it { expect(data[:build_status]).to eq(build.status) } it { expect(data[:build_allow_failure]).to eq(false) } + it { expect(data[:build_failure_reason]).to eq(build.failure_reason) } it { expect(data[:project_id]).to eq(build.project.id) } it { expect(data[:project_name]).to eq(build.project.full_name) } diff --git a/spec/lib/gitlab/database/migration_helpers_spec.rb b/spec/lib/gitlab/database/migration_helpers_spec.rb index eb7148ff108..4e83b27e4a5 100644 --- a/spec/lib/gitlab/database/migration_helpers_spec.rb +++ b/spec/lib/gitlab/database/migration_helpers_spec.rb @@ -48,10 +48,10 @@ describe Gitlab::Database::MigrationHelpers do allow(model).to receive(:transaction_open?).and_return(false) end - context 'using PostgreSQL' do + context 'using PostgreSQL', :postgresql do before do allow(Gitlab::Database).to receive(:postgresql?).and_return(true) - allow(model).to receive(:disable_statement_timeout) + allow(model).to receive(:disable_statement_timeout).and_call_original end it 'creates the index concurrently' do @@ -114,12 +114,12 @@ describe Gitlab::Database::MigrationHelpers do before do allow(model).to receive(:transaction_open?).and_return(false) allow(model).to receive(:index_exists?).and_return(true) + allow(model).to receive(:disable_statement_timeout).and_call_original end context 'using PostgreSQL' do before do allow(model).to receive(:supports_drop_index_concurrently?).and_return(true) - allow(model).to receive(:disable_statement_timeout) end describe 'by column name' do @@ -162,7 +162,7 @@ describe Gitlab::Database::MigrationHelpers do context 'using MySQL' do it 'removes an index' do - expect(Gitlab::Database).to receive(:postgresql?).and_return(false) + expect(Gitlab::Database).to receive(:postgresql?).and_return(false).twice expect(model).to receive(:remove_index) .with(:users, { column: :foo }) @@ -224,21 +224,26 @@ describe Gitlab::Database::MigrationHelpers do context 'using PostgreSQL' do before do + allow(Gitlab::Database).to receive(:postgresql?).and_return(true) allow(Gitlab::Database).to receive(:mysql?).and_return(false) end it 'creates a concurrent foreign key and validates it' do - expect(model).to receive(:disable_statement_timeout) + expect(model).to receive(:disable_statement_timeout).and_call_original + expect(model).to receive(:execute).with(/statement_timeout/) expect(model).to receive(:execute).ordered.with(/NOT VALID/) expect(model).to receive(:execute).ordered.with(/VALIDATE CONSTRAINT/) + expect(model).to receive(:execute).with(/RESET ALL/) model.add_concurrent_foreign_key(:projects, :users, column: :user_id) end it 'appends a valid ON DELETE statement' do - expect(model).to receive(:disable_statement_timeout) + expect(model).to receive(:disable_statement_timeout).and_call_original + expect(model).to receive(:execute).with(/statement_timeout/) expect(model).to receive(:execute).with(/ON DELETE SET NULL/) expect(model).to receive(:execute).ordered.with(/VALIDATE CONSTRAINT/) + expect(model).to receive(:execute).with(/RESET ALL/) model.add_concurrent_foreign_key(:projects, :users, column: :user_id, @@ -291,13 +296,68 @@ describe Gitlab::Database::MigrationHelpers do describe '#disable_statement_timeout' do context 'using PostgreSQL' do - it 'disables statement timeouts' do + it 'disables statement timeouts to current transaction only' do expect(Gitlab::Database).to receive(:postgresql?).and_return(true) - expect(model).to receive(:execute).with('SET statement_timeout TO 0') + expect(model).to receive(:execute).with('SET LOCAL statement_timeout TO 0') model.disable_statement_timeout end + + # this specs runs without an enclosing transaction (:delete truncation method for db_cleaner) + context 'with real environment', :postgresql, :delete do + before do + model.execute("SET statement_timeout TO '20000'") + end + + after do + model.execute('RESET ALL') + end + + it 'defines statement to 0 only for current transaction' do + expect(model.execute('SHOW statement_timeout').first['statement_timeout']).to eq('20s') + + model.connection.transaction do + model.disable_statement_timeout + expect(model.execute('SHOW statement_timeout').first['statement_timeout']).to eq('0') + end + + expect(model.execute('SHOW statement_timeout').first['statement_timeout']).to eq('20s') + end + end + + context 'when passing a blocks' do + it 'disables statement timeouts on session level and executes the block' do + expect(Gitlab::Database).to receive(:postgresql?).and_return(true) + expect(model).to receive(:execute).with('SET statement_timeout TO 0') + expect(model).to receive(:execute).with('RESET ALL') + + expect { |block| model.disable_statement_timeout(&block) }.to yield_control + end + + # this specs runs without an enclosing transaction (:delete truncation method for db_cleaner) + context 'with real environment', :postgresql, :delete do + before do + model.execute("SET statement_timeout TO '20000'") + end + + after do + model.execute('RESET ALL') + end + + it 'defines statement to 0 for any code run inside the block' do + expect(model.execute('SHOW statement_timeout').first['statement_timeout']).to eq('20s') + + model.disable_statement_timeout do + model.connection.transaction do + expect(model.execute('SHOW statement_timeout').first['statement_timeout']).to eq('0') + end + + expect(model.execute('SHOW statement_timeout').first['statement_timeout']).to eq('0') + end + end + end + end end context 'using MySQL' do @@ -308,6 +368,16 @@ describe Gitlab::Database::MigrationHelpers do model.disable_statement_timeout end + + context 'when passing a blocks' do + it 'executes the block of code' do + expect(Gitlab::Database).to receive(:postgresql?).and_return(false) + + expect(model).not_to receive(:execute) + + expect { |block| model.disable_statement_timeout(&block) }.to yield_control + end + end end end diff --git a/spec/lib/gitlab/git/merge_base_spec.rb b/spec/lib/gitlab/git/merge_base_spec.rb new file mode 100644 index 00000000000..2f4e043a20f --- /dev/null +++ b/spec/lib/gitlab/git/merge_base_spec.rb @@ -0,0 +1,95 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::Git::MergeBase do + set(:project) { create(:project, :repository) } + let(:repository) { project.repository } + subject(:merge_base) { described_class.new(repository, refs) } + + shared_context 'existing refs with a merge base', :existing_refs do + let(:refs) do + %w(304d257dcb821665ab5110318fc58a007bd104ed 0031876facac3f2b2702a0e53a26e89939a42209) + end + end + + shared_context 'when passing a missing ref', :missing_ref do + let(:refs) do + %w(304d257dcb821665ab5110318fc58a007bd104ed aaaa) + end + end + + shared_context 'when passing refs that do not have a common ancestor', :no_common_ancestor do + let(:refs) { ['304d257dcb821665ab5110318fc58a007bd104ed', TestEnv::BRANCH_SHA['orphaned-branch']] } + end + + describe '#sha' do + context 'when the refs exist', :existing_refs do + it 'returns the SHA of the merge base' do + expect(merge_base.sha).not_to be_nil + end + + it 'memoizes the result' do + expect(repository).to receive(:merge_base).once.and_call_original + + 2.times { merge_base.sha } + end + end + + context 'when passing a missing ref', :missing_ref do + it 'does not call merge_base on the repository but raises an error' do + expect(repository).not_to receive(:merge_base) + + expect { merge_base.sha }.to raise_error(Gitlab::Git::UnknownRef) + end + end + + it 'returns `nil` when the refs do not have a common ancestor', :no_common_ancestor do + expect(merge_base.sha).to be_nil + end + + it 'returns a merge base when passing 2 branch names' do + merge_base = described_class.new(repository, %w(master feature)) + + expect(merge_base.sha).to be_present + end + + it 'returns a merge base when passing a tag name' do + merge_base = described_class.new(repository, %w(master v1.0.0)) + + expect(merge_base.sha).to be_present + end + end + + describe '#commit' do + context 'for existing refs with a merge base', :existing_refs do + it 'finds the commit for the merge base' do + expect(merge_base.commit).to be_a(Commit) + end + + it 'only looks up the commit once' do + expect(repository).to receive(:commit_by).once.and_call_original + + 2.times { merge_base.commit } + end + end + + it 'does not try to find the commit when there is no sha', :no_common_ancestor do + expect(repository).not_to receive(:commit_by) + + merge_base.commit + end + end + + describe '#unknown_refs', :missing_ref do + it 'returns the the refs passed that are not part of the repository' do + expect(merge_base.unknown_refs).to contain_exactly('aaaa') + end + + it 'only looks up the commits once' do + expect(merge_base).to receive(:commits_for_refs).once.and_call_original + + 2.times { merge_base.unknown_refs } + end + end +end diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index 35a6fc94753..17348b01006 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -542,7 +542,7 @@ describe Gitlab::Git::Repository, :seed_helper do Gitlab::Shell.new.remove_repository('default', 'my_project') end - shared_examples 'repository mirror fecthing' do + shared_examples 'repository mirror fetching' do it 'fetches a repository as a mirror remote' do subject @@ -569,11 +569,11 @@ describe Gitlab::Git::Repository, :seed_helper do end context 'with gitaly enabled' do - it_behaves_like 'repository mirror fecthing' + it_behaves_like 'repository mirror fetching' end context 'with gitaly enabled', :skip_gitaly_mock do - it_behaves_like 'repository mirror fecthing' + it_behaves_like 'repository mirror fetching' end def new_repository_path diff --git a/spec/lib/gitlab/gitaly_client/diff_spec.rb b/spec/lib/gitlab/gitaly_client/diff_spec.rb index 00a31ac0b96..ec7ab2fdedb 100644 --- a/spec/lib/gitlab/gitaly_client/diff_spec.rb +++ b/spec/lib/gitlab/gitaly_client/diff_spec.rb @@ -9,7 +9,9 @@ describe Gitlab::GitalyClient::Diff do new_mode: 0100644, from_id: '357406f3075a57708d0163752905cc1576fceacc', to_id: '8e5177d718c561d36efde08bad36b43687ee6bf0', - patch: 'a' * 100 + patch: 'a' * 100, + collapsed: false, + too_large: false } end @@ -22,6 +24,8 @@ describe Gitlab::GitalyClient::Diff do it { is_expected.to respond_to(:from_id) } it { is_expected.to respond_to(:to_id) } it { is_expected.to respond_to(:patch) } + it { is_expected.to respond_to(:collapsed) } + it { is_expected.to respond_to(:too_large) } describe '#==' do it { expect(subject).to eq(described_class.new(diff_fields)) } diff --git a/spec/lib/gitlab/github_import/bulk_importing_spec.rb b/spec/lib/gitlab/github_import/bulk_importing_spec.rb index 91229d9c7d4..861710f7e9b 100644 --- a/spec/lib/gitlab/github_import/bulk_importing_spec.rb +++ b/spec/lib/gitlab/github_import/bulk_importing_spec.rb @@ -58,5 +58,17 @@ describe Gitlab::GithubImport::BulkImporting do importer.bulk_insert(model, rows, batch_size: 5) end + + it 'calls pre_hook for each slice if given' do + rows = [{ title: 'Foo' }] * 10 + model = double(:model, table_name: 'kittens') + pre_hook = double('pre_hook', call: nil) + allow(Gitlab::Database).to receive(:bulk_insert) + + expect(pre_hook).to receive(:call).with(rows[0..4]) + expect(pre_hook).to receive(:call).with(rows[5..9]) + + importer.bulk_insert(model, rows, batch_size: 5, pre_hook: pre_hook) + end end end diff --git a/spec/lib/gitlab/github_import/importer/issue_importer_spec.rb b/spec/lib/gitlab/github_import/importer/issue_importer_spec.rb index 81fe97c1e49..3f7a12144d5 100644 --- a/spec/lib/gitlab/github_import/importer/issue_importer_spec.rb +++ b/spec/lib/gitlab/github_import/importer/issue_importer_spec.rb @@ -78,6 +78,11 @@ describe Gitlab::GithubImport::Importer::IssueImporter, :clean_gitlab_redis_cach .to receive(:id_for) .with(issue) .and_return(milestone.id) + + allow(importer.user_finder) + .to receive(:author_id_for) + .with(issue) + .and_return([user.id, true]) end context 'when the issue author could be found' do @@ -172,6 +177,23 @@ describe Gitlab::GithubImport::Importer::IssueImporter, :clean_gitlab_redis_cach expect(importer.create_issue).to be_a_kind_of(Numeric) end + + it 'triggers internal_id functionality to track greatest iids' do + allow(importer.user_finder) + .to receive(:author_id_for) + .with(issue) + .and_return([user.id, true]) + + issue = build_stubbed(:issue, project: project) + allow(Gitlab::GithubImport) + .to receive(:insert_and_return_id) + .and_return(issue.id) + allow(project.issues).to receive(:find).with(issue.id).and_return(issue) + + expect(issue).to receive(:ensure_project_iid!) + + importer.create_issue + end end describe '#create_assignees' do diff --git a/spec/lib/gitlab/github_import/importer/milestones_importer_spec.rb b/spec/lib/gitlab/github_import/importer/milestones_importer_spec.rb index b1cac3b6e46..db0be760c7b 100644 --- a/spec/lib/gitlab/github_import/importer/milestones_importer_spec.rb +++ b/spec/lib/gitlab/github_import/importer/milestones_importer_spec.rb @@ -29,13 +29,25 @@ describe Gitlab::GithubImport::Importer::MilestonesImporter, :clean_gitlab_redis expect(importer) .to receive(:bulk_insert) - .with(Milestone, [milestone_hash]) + .with(Milestone, [milestone_hash], any_args) expect(importer) .to receive(:build_milestones_cache) importer.execute end + + it 'tracks internal ids' do + milestone_hash = { iid: 1, title: '1.0', project_id: project.id } + allow(importer) + .to receive(:build_milestones) + .and_return([milestone_hash]) + + expect(InternalId).to receive(:track_greatest) + .with(nil, { project: project }, :milestones, 1, any_args) + + importer.execute + end end describe '#build_milestones' do diff --git a/spec/lib/gitlab/github_import/importer/pull_request_importer_spec.rb b/spec/lib/gitlab/github_import/importer/pull_request_importer_spec.rb index 3422a1e82fc..44c920043b4 100644 --- a/spec/lib/gitlab/github_import/importer/pull_request_importer_spec.rb +++ b/spec/lib/gitlab/github_import/importer/pull_request_importer_spec.rb @@ -111,6 +111,16 @@ describe Gitlab::GithubImport::Importer::PullRequestImporter, :clean_gitlab_redi expect(mr).to be_instance_of(MergeRequest) expect(exists).to eq(false) end + + it 'triggers internal_id functionality to track greatest iids' do + mr = build_stubbed(:merge_request, source_project: project, target_project: project) + allow(Gitlab::GithubImport).to receive(:insert_and_return_id).and_return(mr.id) + allow(project.merge_requests).to receive(:find).with(mr.id).and_return(mr) + + expect(mr).to receive(:ensure_target_project_iid!) + + importer.create_merge_request + end end context 'when the author could not be found' do diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index e9a1932407d..b4269bd5786 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -210,7 +210,6 @@ project: - flowdock_service - assembla_service - asana_service -- gemnasium_service - slack_service - microsoft_teams_service - mattermost_service diff --git a/spec/lib/gitlab/template/finders/repo_template_finders_spec.rb b/spec/lib/gitlab/template/finders/repo_template_finders_spec.rb new file mode 100644 index 00000000000..2eabccd5dff --- /dev/null +++ b/spec/lib/gitlab/template/finders/repo_template_finders_spec.rb @@ -0,0 +1,37 @@ +require 'spec_helper' + +describe Gitlab::Template::Finders::RepoTemplateFinder do + set(:project) { create(:project, :repository) } + + let(:categories) { { 'HTML' => 'html' } } + + subject(:finder) { described_class.new(project, 'files/', '.html', categories) } + + describe '#read' do + it 'returns the content of the given path' do + result = finder.read('files/html/500.html') + + expect(result).to be_present + end + + it 'raises an error if the path does not exist' do + expect { finder.read('does/not/exist') }.to raise_error(described_class::FileNotFoundError) + end + end + + describe '#find' do + it 'returns the full path of the found template' do + result = finder.find('500') + + expect(result).to eq('files/html/500.html') + end + end + + describe '#list_files_for' do + it 'returns the full path of the found files' do + result = finder.list_files_for('files/html') + + expect(result).to contain_exactly('files/html/500.html') + end + end +end diff --git a/spec/lib/system_check/simple_executor_spec.rb b/spec/lib/system_check/simple_executor_spec.rb index 9da3648400e..e71e9da369d 100644 --- a/spec/lib/system_check/simple_executor_spec.rb +++ b/spec/lib/system_check/simple_executor_spec.rb @@ -98,15 +98,6 @@ describe SystemCheck::SimpleExecutor do end end - before do - @rainbow = Rainbow.enabled - Rainbow.enabled = false - end - - after do - Rainbow.enabled = @rainbow - end - describe '#component' do it 'returns stored component name' do expect(subject.component).to eq('Test') diff --git a/spec/migrations/delete_inconsistent_internal_id_records_spec.rb b/spec/migrations/delete_inconsistent_internal_id_records_spec.rb new file mode 100644 index 00000000000..becb71cf427 --- /dev/null +++ b/spec/migrations/delete_inconsistent_internal_id_records_spec.rb @@ -0,0 +1,119 @@ +# frozen_string_literal: true +# rubocop:disable RSpec/FactoriesInMigrationSpecs +require 'spec_helper' +require Rails.root.join('db', 'post_migrate', '20180723130817_delete_inconsistent_internal_id_records.rb') + +describe DeleteInconsistentInternalIdRecords, :migration do + let!(:project1) { create(:project) } + let!(:project2) { create(:project) } + let!(:project3) { create(:project) } + + let(:internal_id_query) { ->(project) { InternalId.where(usage: InternalId.usages[scope.to_s.tableize], project: project) } } + + let(:create_models) do + 3.times { create(scope, project: project1) } + 3.times { create(scope, project: project2) } + 3.times { create(scope, project: project3) } + end + + shared_examples_for 'deleting inconsistent internal_id records' do + before do + create_models + + internal_id_query.call(project1).first.tap do |iid| + iid.last_value = iid.last_value - 2 + # This is an inconsistent record + iid.save! + end + + internal_id_query.call(project3).first.tap do |iid| + iid.last_value = iid.last_value + 2 + # This is a consistent record + iid.save! + end + end + + it "deletes inconsistent issues" do + expect { migrate! }.to change { internal_id_query.call(project1).size }.from(1).to(0) + end + + it "retains consistent issues" do + expect { migrate! }.not_to change { internal_id_query.call(project2).size } + end + + it "retains consistent records, especially those with a greater last_value" do + expect { migrate! }.not_to change { internal_id_query.call(project3).size } + end + end + + context 'for issues' do + let(:scope) { :issue } + it_behaves_like 'deleting inconsistent internal_id records' + end + + context 'for merge_requests' do + let(:scope) { :merge_request } + + let(:create_models) do + 3.times { |i| create(scope, target_project: project1, source_project: project1, source_branch: i.to_s) } + 3.times { |i| create(scope, target_project: project2, source_project: project2, source_branch: i.to_s) } + 3.times { |i| create(scope, target_project: project3, source_project: project3, source_branch: i.to_s) } + end + + it_behaves_like 'deleting inconsistent internal_id records' + end + + context 'for deployments' do + let(:scope) { :deployment } + it_behaves_like 'deleting inconsistent internal_id records' + end + + context 'for milestones (by project)' do + let(:scope) { :milestone } + it_behaves_like 'deleting inconsistent internal_id records' + end + + context 'for ci_pipelines' do + let(:scope) { :ci_pipeline } + it_behaves_like 'deleting inconsistent internal_id records' + end + + context 'for milestones (by group)' do + # milestones (by group) is a little different than all of the other models + let!(:group1) { create(:group) } + let!(:group2) { create(:group) } + let!(:group3) { create(:group) } + + let(:internal_id_query) { ->(group) { InternalId.where(usage: InternalId.usages['milestones'], namespace: group) } } + + before do + 3.times { create(:milestone, group: group1) } + 3.times { create(:milestone, group: group2) } + 3.times { create(:milestone, group: group3) } + + internal_id_query.call(group1).first.tap do |iid| + iid.last_value = iid.last_value - 2 + # This is an inconsistent record + iid.save! + end + + internal_id_query.call(group3).first.tap do |iid| + iid.last_value = iid.last_value + 2 + # This is a consistent record + iid.save! + end + end + + it "deletes inconsistent issues" do + expect { migrate! }.to change { internal_id_query.call(group1).size }.from(1).to(0) + end + + it "retains consistent issues" do + expect { migrate! }.not_to change { internal_id_query.call(group2).size } + end + + it "retains consistent records, especially those with a greater last_value" do + expect { migrate! }.not_to change { internal_id_query.call(group3).size } + end + end +end diff --git a/spec/migrations/migrate_null_wiki_access_levels_spec.rb b/spec/migrations/migrate_null_wiki_access_levels_spec.rb new file mode 100644 index 00000000000..f99273072a2 --- /dev/null +++ b/spec/migrations/migrate_null_wiki_access_levels_spec.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +require 'spec_helper' +require Rails.root.join('db', 'post_migrate', '20180809195358_migrate_null_wiki_access_levels.rb') + +describe MigrateNullWikiAccessLevels, :migration do + let(:namespaces) { table('namespaces') } + let(:projects) { table(:projects) } + let(:project_features) { table(:project_features) } + let(:migration) { described_class.new } + + before do + namespace = namespaces.create(name: 'foo', path: 'foo') + + projects.create!(id: 1, name: 'gitlab1', path: 'gitlab1', namespace_id: namespace.id) + projects.create!(id: 2, name: 'gitlab2', path: 'gitlab2', namespace_id: namespace.id) + projects.create!(id: 3, name: 'gitlab3', path: 'gitlab3', namespace_id: namespace.id) + + project_features.create!(id: 1, project_id: 1, wiki_access_level: nil) + project_features.create!(id: 2, project_id: 2, wiki_access_level: 10) + project_features.create!(id: 3, project_id: 3, wiki_access_level: 20) + end + + describe '#up' do + it 'migrates existing project_features with wiki_access_level NULL to 20' do + expect { migration.up }.to change { project_features.where(wiki_access_level: 20).count }.by(1) + end + end +end diff --git a/spec/migrations/schedule_create_gpg_key_subkeys_from_gpg_keys_spec.rb b/spec/migrations/schedule_create_gpg_key_subkeys_from_gpg_keys_spec.rb index 96bef107599..c4427910518 100644 --- a/spec/migrations/schedule_create_gpg_key_subkeys_from_gpg_keys_spec.rb +++ b/spec/migrations/schedule_create_gpg_key_subkeys_from_gpg_keys_spec.rb @@ -6,7 +6,7 @@ describe ScheduleCreateGpgKeySubkeysFromGpgKeys, :migration, :sidekiq do create(:gpg_key, id: 1, key: GpgHelpers::User1.public_key) # rubocop:disable RSpec/FactoriesInMigrationSpecs create(:gpg_key, id: 2, key: GpgHelpers::User3.public_key) # rubocop:disable RSpec/FactoriesInMigrationSpecs # Delete all subkeys so they can be recreated - GpgKeySubkey.destroy_all + GpgKeySubkey.destroy_all # rubocop: disable DestroyAll end it 'correctly schedules background migrations' do diff --git a/spec/models/award_emoji_spec.rb b/spec/models/award_emoji_spec.rb index b909e04dfc3..3f52091698c 100644 --- a/spec/models/award_emoji_spec.rb +++ b/spec/models/award_emoji_spec.rb @@ -77,4 +77,27 @@ describe AwardEmoji do end end end + + describe '.award_counts_for_user' do + let(:user) { create(:user) } + + before do + create(:award_emoji, user: user, name: 'thumbsup') + create(:award_emoji, user: user, name: 'thumbsup') + create(:award_emoji, user: user, name: 'thumbsdown') + create(:award_emoji, user: user, name: '+1') + end + + it 'returns the awarded emoji in descending order' do + awards = described_class.award_counts_for_user(user) + + expect(awards).to eq('thumbsup' => 2, 'thumbsdown' => 1, '+1' => 1) + end + + it 'limits the returned number of rows' do + awards = described_class.award_counts_for_user(user, 1) + + expect(awards).to eq('thumbsup' => 2) + end + end end diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index 32b8755ee9a..42b627b6823 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -1103,6 +1103,12 @@ describe Ci::Build do it { is_expected.to be_cancelable } end + + context 'when build is created' do + let(:build) { create(:ci_build, :created) } + + it { is_expected.to be_cancelable } + end end context 'when build is not cancelable' do diff --git a/spec/models/concerns/optionally_search_spec.rb b/spec/models/concerns/optionally_search_spec.rb new file mode 100644 index 00000000000..ff4212ddf18 --- /dev/null +++ b/spec/models/concerns/optionally_search_spec.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe OptionallySearch do + let(:model) do + Class.new(ActiveRecord::Base) do + self.table_name = 'users' + + include OptionallySearch + end + end + + describe '.search' do + it 'raises NotImplementedError' do + expect { model.search('foo') }.to raise_error(NotImplementedError) + end + end + + describe '.optionally_search' do + context 'when a query is given' do + it 'delegates to the search method' do + expect(model) + .to receive(:search) + .with('foo') + + model.optionally_search('foo') + end + end + + context 'when no query is given' do + it 'returns the current relation' do + expect(model.optionally_search).to be_a_kind_of(ActiveRecord::Relation) + end + end + + context 'when an empty query is given' do + it 'returns the current relation' do + expect(model.optionally_search('')) + .to be_a_kind_of(ActiveRecord::Relation) + end + end + end +end diff --git a/spec/models/fork_network_member_spec.rb b/spec/models/fork_network_member_spec.rb index 25bf596fddc..60d04562e6c 100644 --- a/spec/models/fork_network_member_spec.rb +++ b/spec/models/fork_network_member_spec.rb @@ -11,7 +11,7 @@ describe ForkNetworkMember do let(:fork_network) { fork_network_member.fork_network } it 'removes the fork network if it was the last member' do - fork_network.fork_network_members.destroy_all + fork_network.fork_network_members.destroy_all # rubocop: disable DestroyAll expect(ForkNetwork.count).to eq(0) end diff --git a/spec/models/hooks/system_hook_spec.rb b/spec/models/hooks/system_hook_spec.rb index 01129df1107..edd1cb455af 100644 --- a/spec/models/hooks/system_hook_spec.rb +++ b/spec/models/hooks/system_hook_spec.rb @@ -73,7 +73,7 @@ describe SystemHook do it "project_destroy hook" do project.add_maintainer(user) - project.project_members.destroy_all + project.project_members.destroy_all # rubocop: disable DestroyAll expect(WebMock).to have_requested(:post, system_hook.url).with( body: /user_remove_from_team/, @@ -110,7 +110,7 @@ describe SystemHook do it 'group member destroy hook' do group.add_maintainer(user) - group.group_members.destroy_all + group.group_members.destroy_all # rubocop: disable DestroyAll expect(WebMock).to have_requested(:post, system_hook.url).with( body: /user_remove_from_group/, diff --git a/spec/models/internal_id_spec.rb b/spec/models/internal_id_spec.rb index 20600f5fa38..f2aad455d5f 100644 --- a/spec/models/internal_id_spec.rb +++ b/spec/models/internal_id_spec.rb @@ -30,7 +30,7 @@ describe InternalId do context 'with existing issues' do before do - rand(1..10).times { create(:issue, project: project) } + create_list(:issue, 2, project: project) described_class.delete_all end @@ -54,7 +54,7 @@ describe InternalId do end it 'generates a strictly monotone, gapless sequence' do - seq = (0..rand(100)).map do + seq = Array.new(10).map do described_class.generate_next(issue, scope, usage, init) end normalized = seq.map { |i| i - seq.min } diff --git a/spec/models/license_template_spec.rb b/spec/models/license_template_spec.rb new file mode 100644 index 00000000000..c633e1908d4 --- /dev/null +++ b/spec/models/license_template_spec.rb @@ -0,0 +1,59 @@ +require 'spec_helper' + +describe LicenseTemplate do + describe '#content' do + it 'calls a proc exactly once if provided' do + lazy = build_template(-> { 'bar' }) + content = lazy.content + + expect(content).to eq('bar') + expect(content.object_id).to eq(lazy.content.object_id) + + content.replace('foo') + expect(lazy.content).to eq('foo') + end + + it 'returns a string if provided' do + lazy = build_template('bar') + + expect(lazy.content).to eq('bar') + end + end + + describe '#resolve!' do + let(:content) do + <<~TEXT + Pretend License + + [project] + + Copyright (c) [year] [fullname] + TEXT + end + + let(:expected) do + <<~TEXT + Pretend License + + Foo Project + + Copyright (c) 1985 Nick Thomas + TEXT + end + + let(:template) { build_template(content) } + + it 'updates placeholders in a copy of the template content' do + expect(template.content.object_id).to eq(content.object_id) + + template.resolve!(project_name: "Foo Project", fullname: "Nick Thomas", year: "1985") + + expect(template.content).to eq(expected) + expect(template.content.object_id).not_to eq(content.object_id) + end + end + + def build_template(content) + described_class.new(id: 'foo', name: 'foo', category: :Other, content: content) + end +end diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index 6258bfa232f..48f4e53b93e 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -1764,7 +1764,7 @@ describe MergeRequest do context 'with no discussions' do before do - merge_request.notes.destroy_all + merge_request.notes.destroy_all # rubocop: disable DestroyAll end it 'returns true' do diff --git a/spec/models/notification_setting_spec.rb b/spec/models/notification_setting_spec.rb index 77c475b9f52..e545b674b4f 100644 --- a/spec/models/notification_setting_spec.rb +++ b/spec/models/notification_setting_spec.rb @@ -94,9 +94,39 @@ RSpec.describe NotificationSetting do end end - context 'email events' do - it 'includes EXCLUDED_WATCHER_EVENTS in EMAIL_EVENTS' do - expect(described_class::EMAIL_EVENTS).to include(*described_class::EXCLUDED_WATCHER_EVENTS) + describe '.email_events' do + subject { described_class.email_events } + + it 'returns email events' do + expect(subject).to include( + :new_note, + :new_issue, + :reopen_issue, + :close_issue, + :reassign_issue, + :new_merge_request, + :reopen_merge_request, + :close_merge_request, + :reassign_merge_request, + :merge_merge_request, + :failed_pipeline, + :success_pipeline + ) + end + + it 'includes EXCLUDED_WATCHER_EVENTS' do + expect(subject).to include(*described_class::EXCLUDED_WATCHER_EVENTS) + end + end + + describe '#email_events' do + let(:source) { build(:group) } + + subject { build(:notification_setting, source: source) } + + it 'calls email_events' do + expect(described_class).to receive(:email_events).with(source) + subject.email_events end end end diff --git a/spec/models/project_auto_devops_spec.rb b/spec/models/project_auto_devops_spec.rb index 749b2094787..797d767465a 100644 --- a/spec/models/project_auto_devops_spec.rb +++ b/spec/models/project_auto_devops_spec.rb @@ -100,7 +100,7 @@ describe ProjectAutoDevops do end end - describe '#set_gitlab_deploy_token' do + describe '#create_gitlab_deploy_token' do let(:auto_devops) { build(:project_auto_devops, project: project) } context 'when the project is public' do @@ -144,9 +144,9 @@ describe ProjectAutoDevops do end end - context 'when autodevops is enabled at instancel level' do + context 'when autodevops is enabled at instance level' do let(:project) { create(:project, :repository, :internal) } - let(:auto_devops) { build(:project_auto_devops, :disabled, project: project) } + let(:auto_devops) { build(:project_auto_devops, enabled: nil, project: project) } it 'should create a deploy token' do allow(Gitlab::CurrentSettings).to receive(:auto_devops_enabled?).and_return(true) diff --git a/spec/models/project_group_link_spec.rb b/spec/models/project_group_link_spec.rb index 1fccf92627a..5bea21427d4 100644 --- a/spec/models/project_group_link_spec.rb +++ b/spec/models/project_group_link_spec.rb @@ -41,7 +41,7 @@ describe ProjectGroupLink do project.project_group_links.create(group: group) group_users.each { |user| expect(user.authorized_projects).to include(project) } - project.project_group_links.destroy_all + project.project_group_links.destroy_all # rubocop: disable DestroyAll group_users.each { |user| expect(user.authorized_projects).not_to include(project) } end end diff --git a/spec/models/project_services/gemnasium_service_spec.rb b/spec/models/project_services/gemnasium_service_spec.rb deleted file mode 100644 index 6e417323e40..00000000000 --- a/spec/models/project_services/gemnasium_service_spec.rb +++ /dev/null @@ -1,74 +0,0 @@ -require 'spec_helper' - -describe GemnasiumService do - describe "Associations" do - it { is_expected.to belong_to :project } - it { is_expected.to have_one :service_hook } - end - - describe 'Validations' do - context 'when service is active' do - before do - subject.active = true - end - - it { is_expected.to validate_presence_of(:token) } - it { is_expected.to validate_presence_of(:api_key) } - end - - context 'when service is inactive' do - before do - subject.active = false - end - - it { is_expected.not_to validate_presence_of(:token) } - it { is_expected.not_to validate_presence_of(:api_key) } - end - end - - describe "deprecated?" do - let(:project) { create(:project, :repository) } - let(:gemnasium_service) { described_class.new } - - before do - allow(gemnasium_service).to receive_messages( - project_id: project.id, - project: project, - service_hook: true, - token: 'verySecret', - api_key: 'GemnasiumUserApiKey' - ) - end - - it "is true" do - expect(gemnasium_service.deprecated?).to be true - end - - it "can't create a new service" do - expect(gemnasium_service.save).to be false - expect(gemnasium_service.errors[:base].first) - .to eq('Gemnasium has been acquired by GitLab in January 2018. Since May 15, 2018, the service provided by Gemnasium is no longer available.') - end - end - - describe "Execute" do - let(:user) { create(:user) } - let(:project) { create(:project, :repository) } - let(:gemnasium_service) { described_class.new } - let(:sample_data) { Gitlab::DataBuilder::Push.build_sample(project, user) } - - before do - allow(gemnasium_service).to receive_messages( - project_id: project.id, - project: project, - service_hook: true, - token: 'verySecret', - api_key: 'GemnasiumUserApiKey' - ) - end - it "calls Gemnasium service" do - expect(Gemnasium::GitlabService).to receive(:execute).with(an_instance_of(Hash)).once - gemnasium_service.execute(sample_data) - end - end -end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 03beb9187ed..8cb706b54b0 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -42,7 +42,6 @@ describe Project do it { is_expected.to have_one(:assembla_service) } it { is_expected.to have_one(:slack_slash_commands_service) } it { is_expected.to have_one(:mattermost_slash_commands_service) } - it { is_expected.to have_one(:gemnasium_service) } it { is_expected.to have_one(:buildkite_service) } it { is_expected.to have_one(:bamboo_service) } it { is_expected.to have_one(:teamcity_service) } @@ -364,6 +363,15 @@ describe Project do it { is_expected.to delegate_method(:name).to(:owner).with_prefix(true).with_arguments(allow_nil: true) } end + describe '#to_reference_with_postfix' do + it 'returns the full path with reference_postfix' do + namespace = create(:namespace, path: 'sample-namespace') + project = create(:project, path: 'sample-project', namespace: namespace) + + expect(project.to_reference_with_postfix).to eq 'sample-namespace/sample-project>' + end + end + describe '#to_reference' do let(:owner) { create(:user, name: 'Gitlab') } let(:namespace) { create(:namespace, path: 'sample-namespace', owner: owner) } @@ -1469,6 +1477,53 @@ describe Project do end end + describe '.optionally_search' do + let(:project) { create(:project) } + + it 'searches for projects matching the query if one is given' do + relation = described_class.optionally_search(project.name) + + expect(relation).to eq([project]) + end + + it 'returns the current relation if no search query is given' do + relation = described_class.where(id: project.id) + + expect(relation.optionally_search).to eq(relation) + end + end + + describe '.paginate_in_descending_order_using_id' do + let!(:project1) { create(:project) } + let!(:project2) { create(:project) } + + it 'orders the relation in descending order' do + expect(described_class.paginate_in_descending_order_using_id) + .to eq([project2, project1]) + end + + it 'applies a limit to the relation' do + expect(described_class.paginate_in_descending_order_using_id(limit: 1)) + .to eq([project2]) + end + + it 'limits projects by and ID when given' do + expect(described_class.paginate_in_descending_order_using_id(before: project2.id)) + .to eq([project1]) + end + end + + describe '.including_namespace_and_owner' do + it 'eager loads the namespace and namespace owner' do + create(:project) + + row = described_class.eager_load_namespace_and_owner.to_a.first + recorder = ActiveRecord::QueryRecorder.new { row.namespace.owner } + + expect(recorder.count).to be_zero + end + end + describe '#expire_caches_before_rename' do let(:project) { create(:project, :repository) } let(:repo) { double(:repo, exists?: true) } @@ -3250,6 +3305,11 @@ describe Project do end describe '#auto_devops_enabled?' do + before do + allow(Feature).to receive(:enabled?).and_call_original + Feature.get(:force_autodevops_on_by_default).enable_percentage_of_actors(0) + end + set(:project) { create(:project) } subject { project.auto_devops_enabled? } @@ -3259,19 +3319,14 @@ describe Project do stub_application_setting(auto_devops_enabled: true) end - it 'auto devops is implicitly enabled' do - expect(project.auto_devops).to be_nil - expect(project).to be_auto_devops_enabled - end + it { is_expected.to be_truthy } context 'when explicitly enabled' do before do create(:project_auto_devops, project: project) end - it "auto devops is enabled" do - expect(project).to be_auto_devops_enabled - end + it { is_expected.to be_truthy } end context 'when explicitly disabled' do @@ -3279,9 +3334,7 @@ describe Project do create(:project_auto_devops, project: project, enabled: false) end - it "auto devops is disabled" do - expect(project).not_to be_auto_devops_enabled - end + it { is_expected.to be_falsey } end end @@ -3290,19 +3343,22 @@ describe Project do stub_application_setting(auto_devops_enabled: false) end - it 'auto devops is implicitly disabled' do - expect(project.auto_devops).to be_nil - expect(project).not_to be_auto_devops_enabled - end + it { is_expected.to be_falsey } context 'when explicitly enabled' do before do create(:project_auto_devops, project: project) end - it "auto devops is enabled" do - expect(project).to be_auto_devops_enabled + it { is_expected.to be_truthy } + end + + context 'when force_autodevops_on_by_default is enabled for the project' do + before do + Feature.get(:force_autodevops_on_by_default).enable_percentage_of_actors(100) end + + it { is_expected.to be_truthy } end end end @@ -3352,6 +3408,11 @@ describe Project do end describe '#has_auto_devops_implicitly_disabled?' do + before do + allow(Feature).to receive(:enabled?).and_call_original + Feature.get(:force_autodevops_on_by_default).enable_percentage_of_actors(0) + end + set(:project) { create(:project) } context 'when enabled in settings' do @@ -3373,6 +3434,16 @@ describe Project do expect(project).to have_auto_devops_implicitly_disabled end + context 'when force_autodevops_on_by_default is enabled for the project' do + before do + Feature.get(:force_autodevops_on_by_default).enable_percentage_of_actors(100) + end + + it 'does not have auto devops implicitly disabled' do + expect(project).not_to have_auto_devops_implicitly_disabled + end + end + context 'when explicitly disabled' do before do create(:project_auto_devops, project: project, enabled: false) diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 52ec8dbe25a..2859d5149ec 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -296,41 +296,31 @@ describe Repository do end describe '#new_commits' do - shared_examples 'finding unreferenced commits' do - set(:project) { create(:project, :repository) } - let(:repository) { project.repository } + set(:project) { create(:project, :repository) } + let(:repository) { project.repository } - subject { repository.new_commits(rev) } + subject { repository.new_commits(rev) } - context 'when there are no new commits' do - let(:rev) { repository.commit.id } + context 'when there are no new commits' do + let(:rev) { repository.commit.id } - it 'returns an empty array' do - expect(subject).to eq([]) - end + it 'returns an empty array' do + expect(subject).to eq([]) end + end - context 'when new commits are found' do - let(:branch) { 'orphaned-branch' } - let!(:rev) { repository.commit(branch).id } + context 'when new commits are found' do + let(:branch) { 'orphaned-branch' } + let!(:rev) { repository.commit(branch).id } - it 'returns the commits' do - repository.delete_branch(branch) + it 'returns the commits' do + repository.delete_branch(branch) - expect(subject).not_to be_empty - expect(subject).to all( be_a(::Commit) ) - expect(subject.size).to eq(1) - end + expect(subject).not_to be_empty + expect(subject).to all( be_a(::Commit) ) + expect(subject.size).to eq(1) end end - - context 'when Gitaly handles the request' do - it_behaves_like 'finding unreferenced commits' - end - - context 'when Gitaly is disabled', :disable_gitaly do - it_behaves_like 'finding unreferenced commits' - end end describe '#commits_by' do diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index f5e2c977104..9763477a711 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -346,17 +346,55 @@ describe User do end end - describe '.todo_authors' do - it 'filters users' do - create :user - user_2 = create :user - user_3 = create :user - current_user = create :user - create(:todo, user: current_user, author: user_2, state: :done) - create(:todo, user: current_user, author: user_3, state: :pending) + describe '.limit_to_todo_authors' do + context 'when filtering by todo authors' do + let(:user1) { create(:user) } + let(:user2) { create(:user) } - expect(described_class.todo_authors(current_user.id, 'pending')).to eq [user_3] - expect(described_class.todo_authors(current_user.id, 'done')).to eq [user_2] + before do + create(:todo, user: user1, author: user1, state: :done) + create(:todo, user: user2, author: user2, state: :pending) + end + + it 'only returns users that have authored todos' do + users = described_class.limit_to_todo_authors( + user: user2, + with_todos: true, + todo_state: :pending + ) + + expect(users).to eq([user2]) + end + + it 'ignores users that do not have a todo in the matching state' do + users = described_class.limit_to_todo_authors( + user: user1, + with_todos: true, + todo_state: :pending + ) + + expect(users).to be_empty + end + end + + context 'when not filtering by todo authors' do + it 'returns the input relation' do + user1 = create(:user) + user2 = create(:user) + rel = described_class.limit_to_todo_authors(user: user1) + + expect(rel).to include(user1, user2) + end + end + + context 'when no user is provided' do + it 'returns the input relation' do + user1 = create(:user) + user2 = create(:user) + rel = described_class.limit_to_todo_authors + + expect(rel).to include(user1, user2) + end end end end @@ -2901,4 +2939,86 @@ describe User do let(:uploader_class) { AttachmentUploader } end end + + describe '.union_with_user' do + context 'when no user ID is provided' do + it 'returns the input relation' do + user = create(:user) + + expect(described_class.union_with_user).to eq([user]) + end + end + + context 'when a user ID is provided' do + it 'includes the user object in the returned relation' do + user1 = create(:user) + user2 = create(:user) + users = described_class.where(id: user1.id).union_with_user(user2.id) + + expect(users).to include(user1) + expect(users).to include(user2) + end + + it 'does not re-apply any WHERE conditions on the outer query' do + relation = described_class.where(id: 1).union_with_user(2) + + expect(relation.arel.where_sql).to be_nil + end + end + end + + describe '.optionally_search' do + context 'using nil as the argument' do + it 'returns the current relation' do + user = create(:user) + + expect(described_class.optionally_search).to eq([user]) + end + end + + context 'using an empty String as the argument' do + it 'returns the current relation' do + user = create(:user) + + expect(described_class.optionally_search('')).to eq([user]) + end + end + + context 'using a non-empty String' do + it 'returns users matching the search query' do + user1 = create(:user) + create(:user) + + expect(described_class.optionally_search(user1.name)).to eq([user1]) + end + end + end + + describe '.where_not_in' do + context 'without an argument' do + it 'returns the current relation' do + user = create(:user) + + expect(described_class.where_not_in).to eq([user]) + end + end + + context 'using a list of user IDs' do + it 'excludes the users from the returned relation' do + user1 = create(:user) + user2 = create(:user) + + expect(described_class.where_not_in([user2.id])).to eq([user1]) + end + end + end + + describe '.reorder_by_name' do + it 'reorders the input relation' do + user1 = create(:user, name: 'A') + user2 = create(:user, name: 'B') + + expect(described_class.reorder_by_name).to eq([user1, user2]) + end + end end diff --git a/spec/policies/group_policy_spec.rb b/spec/policies/group_policy_spec.rb index 35951251bc5..615fea11f26 100644 --- a/spec/policies/group_policy_spec.rb +++ b/spec/policies/group_policy_spec.rb @@ -205,7 +205,7 @@ describe GroupPolicy do nested_group.add_guest(developer) nested_group.add_guest(maintainer) - group.owners.destroy_all + group.owners.destroy_all # rubocop: disable DestroyAll group.add_guest(owner) nested_group.add_owner(owner) diff --git a/spec/requests/api/jobs_spec.rb b/spec/requests/api/jobs_spec.rb index 5814d834572..6adbbb40489 100644 --- a/spec/requests/api/jobs_spec.rb +++ b/spec/requests/api/jobs_spec.rb @@ -3,6 +3,32 @@ require 'spec_helper' describe API::Jobs do include HttpIOHelpers + shared_examples 'a job with artifacts and trace' do |result_is_array: true| + context 'with artifacts and trace' do + let!(:second_job) { create(:ci_build, :trace_artifact, :artifacts, :test_reports, pipeline: pipeline) } + + it 'returns artifacts and trace data', :skip_before_request do + get api(api_endpoint, api_user) + json_job = result_is_array ? json_response.select { |job| job['id'] == second_job.id }.first : json_response + + expect(json_job['artifacts_file']).not_to be_nil + expect(json_job['artifacts_file']).not_to be_empty + expect(json_job['artifacts_file']['filename']).to eq(second_job.artifacts_file.filename) + expect(json_job['artifacts_file']['size']).to eq(second_job.artifacts_file.size) + expect(json_job['artifacts']).not_to be_nil + expect(json_job['artifacts']).to be_an Array + expect(json_job['artifacts'].size).to eq(second_job.job_artifacts.length) + json_job['artifacts'].each do |artifact| + expect(artifact).not_to be_nil + file_type = Ci::JobArtifact.file_types[artifact['file_type']] + expect(artifact['size']).to eq(second_job.job_artifacts.where(file_type: file_type).first.size) + expect(artifact['filename']).to eq(second_job.job_artifacts.where(file_type: file_type).first.filename) + expect(artifact['file_format']).to eq(second_job.job_artifacts.where(file_type: file_type).first.file_format) + end + end + end + end + set(:project) do create(:project, :repository, public_builds: false) end @@ -49,6 +75,20 @@ describe API::Jobs do expect(Time.parse(json_response.first['artifacts_expire_at'])).to be_like_time(job.artifacts_expire_at) end + context 'without artifacts and trace' do + it 'returns no artifacts nor trace data' do + json_job = json_response.first + + expect(json_job['artifacts_file']).to be_nil + expect(json_job['artifacts']).to be_an Array + expect(json_job['artifacts']).to be_empty + end + end + + it_behaves_like 'a job with artifacts and trace' do + let(:api_endpoint) { "/projects/#{project.id}/jobs" } + end + it 'returns pipeline data' do json_job = json_response.first @@ -60,7 +100,7 @@ describe API::Jobs do end it 'avoids N+1 queries', :skip_before_request do - first_build = create(:ci_build, :artifacts, pipeline: pipeline) + first_build = create(:ci_build, :trace_artifact, :artifacts, :test_reports, pipeline: pipeline) first_build.runner = create(:ci_runner) first_build.user = create(:user) first_build.save @@ -68,7 +108,7 @@ describe API::Jobs do control_count = ActiveRecord::QueryRecorder.new { go }.count second_pipeline = create(:ci_empty_pipeline, project: project, sha: project.commit.id, ref: project.default_branch) - second_build = create(:ci_build, :artifacts, pipeline: second_pipeline) + second_build = create(:ci_build, :trace_artifact, :artifacts, :test_reports, pipeline: second_pipeline) second_build.runner = create(:ci_runner) second_build.user = create(:user) second_build.save @@ -117,9 +157,11 @@ describe API::Jobs do describe 'GET /projects/:id/pipelines/:pipeline_id/jobs' do let(:query) { Hash.new } - before do - job - get api("/projects/#{project.id}/pipelines/#{pipeline.id}/jobs", api_user), query + before do |example| + unless example.metadata[:skip_before_request] + job + get api("/projects/#{project.id}/pipelines/#{pipeline.id}/jobs", api_user), query + end end context 'authorized user' do @@ -133,6 +175,13 @@ describe API::Jobs do expect(json_response).not_to be_empty expect(json_response.first['commit']['id']).to eq project.commit.id expect(Time.parse(json_response.first['artifacts_expire_at'])).to be_like_time(job.artifacts_expire_at) + expect(json_response.first['artifacts_file']).to be_nil + expect(json_response.first['artifacts']).to be_an Array + expect(json_response.first['artifacts']).to be_empty + end + + it_behaves_like 'a job with artifacts and trace' do + let(:api_endpoint) { "/projects/#{project.id}/pipelines/#{pipeline.id}/jobs" } end it 'returns pipeline data' do @@ -183,7 +232,7 @@ describe API::Jobs do get api("/projects/#{project.id}/pipelines/#{pipeline.id}/jobs", api_user), query end.count - 3.times { create(:ci_build, :artifacts, pipeline: pipeline) } + 3.times { create(:ci_build, :trace_artifact, :artifacts, :test_reports, pipeline: pipeline) } expect do get api("/projects/#{project.id}/pipelines/#{pipeline.id}/jobs", api_user), query @@ -201,8 +250,10 @@ describe API::Jobs do end describe 'GET /projects/:id/jobs/:job_id' do - before do - get api("/projects/#{project.id}/jobs/#{job.id}", api_user) + before do |example| + unless example.metadata[:skip_before_request] + get api("/projects/#{project.id}/jobs/#{job.id}", api_user) + end end context 'authorized user' do @@ -219,10 +270,17 @@ describe API::Jobs do expect(Time.parse(json_response['started_at'])).to be_like_time(job.started_at) expect(Time.parse(json_response['finished_at'])).to be_like_time(job.finished_at) expect(Time.parse(json_response['artifacts_expire_at'])).to be_like_time(job.artifacts_expire_at) + expect(json_response['artifacts_file']).to be_nil + expect(json_response['artifacts']).to be_an Array + expect(json_response['artifacts']).to be_empty expect(json_response['duration']).to eq(job.duration) expect(json_response['web_url']).to be_present end + it_behaves_like 'a job with artifacts and trace', result_is_array: false do + let(:api_endpoint) { "/projects/#{project.id}/jobs/#{second_job.id}" } + end + it 'returns pipeline data' do json_job = json_response diff --git a/spec/requests/api/project_hooks_spec.rb b/spec/requests/api/project_hooks_spec.rb index bc45a63d9f1..d3f81cc038d 100644 --- a/spec/requests/api/project_hooks_spec.rb +++ b/spec/requests/api/project_hooks_spec.rb @@ -83,11 +83,6 @@ describe API::ProjectHooks, 'ProjectHooks' do expect(response).to have_gitlab_http_status(403) end end - - it "returns a 404 error if hook id is not available" do - get api("/projects/#{project.id}/hooks/1234", user) - expect(response).to have_gitlab_http_status(404) - end end describe "POST /projects/:id/hooks" do diff --git a/spec/requests/api/project_import_spec.rb b/spec/requests/api/project_import_spec.rb index e3fb6cecce9..bc06f3c3732 100644 --- a/spec/requests/api/project_import_spec.rb +++ b/spec/requests/api/project_import_spec.rb @@ -42,7 +42,7 @@ describe API::ProjectImport do expect(response).to have_gitlab_http_status(201) end - it 'does not shedule an import for a nampespace that does not exist' do + it 'does not schedule an import for a namespace that does not exist' do expect_any_instance_of(Project).not_to receive(:import_schedule) expect(::Projects::CreateService).not_to receive(:new) diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index eb41750bf47..c249c881db5 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -20,7 +20,6 @@ describe API::Projects do let(:admin) { create(:admin) } let(:project) { create(:project, :repository, namespace: user.namespace) } let(:project2) { create(:project, namespace: user.namespace) } - let(:snippet) { create(:project_snippet, :public, author: user, project: project, title: 'example') } let(:project_member) { create(:project_member, :developer, user: user3, project: project) } let(:user4) { create(:user) } let(:project3) do @@ -575,7 +574,7 @@ describe API::Projects do expect(json_response['avatar_url']).to eq("http://localhost/uploads/-/system/project/avatar/#{project_id}/banana_sample.gif") end - it 'sets a project as allowing outdated diff discussions to automatically resolve' do + it 'sets a project as not allowing outdated diff discussions to automatically resolve' do project = attributes_for(:project, resolve_outdated_diff_discussions: false) post api('/projects', user), project @@ -583,7 +582,7 @@ describe API::Projects do expect(json_response['resolve_outdated_diff_discussions']).to be_falsey end - it 'sets a project as allowing outdated diff discussions to automatically resolve if resolve_outdated_diff_discussions' do + it 'sets a project as allowing outdated diff discussions to automatically resolve' do project = attributes_for(:project, resolve_outdated_diff_discussions: true) post api('/projects', user), project @@ -698,7 +697,7 @@ describe API::Projects do expect(json_response.map { |project| project['id'] }).to contain_exactly(public_project.id) end - it 'returns projects filetered by minimal access level' do + it 'returns projects filtered by minimal access level' do private_project1 = create(:project, :private, name: 'private_project1', creator_id: user4.id, namespace: user4.namespace) private_project2 = create(:project, :private, name: 'private_project2', creator_id: user4.id, namespace: user4.namespace) private_project1.add_developer(user2) @@ -789,7 +788,7 @@ describe API::Projects do expect(json_response['visibility']).to eq('private') end - it 'sets a project as allowing outdated diff discussions to automatically resolve' do + it 'sets a project as not allowing outdated diff discussions to automatically resolve' do project = attributes_for(:project, resolve_outdated_diff_discussions: false) post api("/projects/user/#{user.id}", admin), project @@ -1119,100 +1118,6 @@ describe API::Projects do end end - describe 'GET /projects/:id/snippets' do - before do - snippet - end - - it 'returns an array of project snippets' do - get api("/projects/#{project.id}/snippets", user) - - expect(response).to have_gitlab_http_status(200) - expect(response).to include_pagination_headers - expect(json_response).to be_an Array - expect(json_response.first['title']).to eq(snippet.title) - end - end - - describe 'GET /projects/:id/snippets/:snippet_id' do - it 'returns a project snippet' do - get api("/projects/#{project.id}/snippets/#{snippet.id}", user) - expect(response).to have_gitlab_http_status(200) - expect(json_response['title']).to eq(snippet.title) - end - - it 'returns a 404 error if snippet id not found' do - get api("/projects/#{project.id}/snippets/1234", user) - expect(response).to have_gitlab_http_status(404) - end - end - - describe 'POST /projects/:id/snippets' do - it 'creates a new project snippet' do - post api("/projects/#{project.id}/snippets", user), - title: 'api test', file_name: 'sample.rb', code: 'test', visibility: 'private' - expect(response).to have_gitlab_http_status(201) - expect(json_response['title']).to eq('api test') - end - - it 'returns a 400 error if invalid snippet is given' do - post api("/projects/#{project.id}/snippets", user) - expect(status).to eq(400) - end - end - - describe 'PUT /projects/:id/snippets/:snippet_id' do - it 'updates an existing project snippet' do - put api("/projects/#{project.id}/snippets/#{snippet.id}", user), - code: 'updated code' - expect(response).to have_gitlab_http_status(200) - expect(json_response['title']).to eq('example') - expect(snippet.reload.content).to eq('updated code') - end - - it 'updates an existing project snippet with new title' do - put api("/projects/#{project.id}/snippets/#{snippet.id}", user), - title: 'other api test' - expect(response).to have_gitlab_http_status(200) - expect(json_response['title']).to eq('other api test') - end - end - - describe 'DELETE /projects/:id/snippets/:snippet_id' do - before do - snippet - end - - it 'deletes existing project snippet' do - expect do - delete api("/projects/#{project.id}/snippets/#{snippet.id}", user) - - expect(response).to have_gitlab_http_status(204) - end.to change { Snippet.count }.by(-1) - end - - it 'returns 404 when deleting unknown snippet id' do - delete api("/projects/#{project.id}/snippets/1234", user) - expect(response).to have_gitlab_http_status(404) - end - - it_behaves_like '412 response' do - let(:request) { api("/projects/#{project.id}/snippets/#{snippet.id}", user) } - end - end - - describe 'GET /projects/:id/snippets/:snippet_id/raw' do - it 'gets a raw project snippet' do - get api("/projects/#{project.id}/snippets/#{snippet.id}/raw", user) - expect(response).to have_gitlab_http_status(200) - end - - it 'returns a 404 error if raw project snippet not found' do - get api("/projects/#{project.id}/snippets/5555/raw", user) - expect(response).to have_gitlab_http_status(404) - end - end - describe 'fork management' do let(:project_fork_target) { create(:project) } let(:project_fork_source) { create(:project, :public) } @@ -1235,7 +1140,7 @@ describe API::Projects do expect(project_fork_target.forked?).to be_truthy end - it 'refreshes the forks count cachce' do + it 'refreshes the forks count cache' do expect(project_fork_source.forks_count).to be_zero post api("/projects/#{project_fork_target.id}/fork/#{project_fork_source.id}", admin) diff --git a/spec/requests/api/repositories_spec.rb b/spec/requests/api/repositories_spec.rb index 6063afc213d..519638ebb82 100644 --- a/spec/requests/api/repositories_spec.rb +++ b/spec/requests/api/repositories_spec.rb @@ -465,4 +465,77 @@ describe API::Repositories do end end end + + describe 'GET :id/repository/merge_base' do + let(:refs) do + %w(304d257dcb821665ab5110318fc58a007bd104ed 0031876facac3f2b2702a0e53a26e89939a42209) + end + + subject(:request) do + get(api("/projects/#{project.id}/repository/merge_base", current_user), refs: refs) + end + + shared_examples 'merge base' do + it 'returns the common ancestor' do + request + + expect(response).to have_gitlab_http_status(:success) + expect(json_response['id']).to be_present + end + end + + context 'when unauthenticated', 'and project is public' do + it_behaves_like 'merge base' do + let(:project) { create(:project, :public, :repository) } + let(:current_user) { nil } + end + end + + context 'when unauthenticated', 'and project is private' do + it_behaves_like '404 response' do + let(:current_user) { nil } + let(:message) { '404 Project Not Found' } + end + end + + context 'when authenticated', 'as a developer' do + it_behaves_like 'merge base' do + let(:current_user) { user } + end + end + + context 'when authenticated', 'as a guest' do + it_behaves_like '403 response' do + let(:current_user) { guest } + end + end + + context 'when passing refs that do not exist' do + it_behaves_like '400 response' do + let(:refs) { %w(304d257dcb821665ab5110318fc58a007bd104ed missing) } + let(:current_user) { user } + let(:message) { 'Could not find ref: missing' } + end + end + + context 'when passing refs that do not have a merge base' do + it_behaves_like '404 response' do + let(:refs) { ['304d257dcb821665ab5110318fc58a007bd104ed', TestEnv::BRANCH_SHA['orphaned-branch']] } + let(:current_user) { user } + let(:message) { '404 Merge Base Not Found' } + end + end + + context 'when not enough refs are passed' do + let(:refs) { %w(only-one) } + let(:current_user) { user } + + it 'renders a bad request error' do + request + + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response['message']).to eq('Provide exactly 2 refs') + end + end + end end diff --git a/spec/requests/api/templates_spec.rb b/spec/requests/api/templates_spec.rb index 6bb53fdc98d..d1e16ab9ca9 100644 --- a/spec/requests/api/templates_spec.rb +++ b/spec/requests/api/templates_spec.rb @@ -56,6 +56,8 @@ describe API::Templates do end it 'returns a license template' do + expect(response).to have_gitlab_http_status(200) + expect(json_response['key']).to eq('mit') expect(json_response['name']).to eq('MIT License') expect(json_response['nickname']).to be_nil @@ -181,6 +183,7 @@ describe API::Templates do it 'replaces the copyright owner placeholder with the name of the current user' do get api('/templates/licenses/mit', user) + expect(response).to have_gitlab_http_status(200) expect(json_response['content']).to include("Copyright (c) #{Time.now.year} #{user.name}") end end diff --git a/spec/rubocop/cop/destroy_all_spec.rb b/spec/rubocop/cop/destroy_all_spec.rb new file mode 100644 index 00000000000..b0bc40552b3 --- /dev/null +++ b/spec/rubocop/cop/destroy_all_spec.rb @@ -0,0 +1,43 @@ +require 'spec_helper' +require 'rubocop' +require 'rubocop/rspec/support' +require_relative '../../../rubocop/cop/destroy_all' + +describe RuboCop::Cop::DestroyAll do + include CopHelper + + subject(:cop) { described_class.new } + + it 'flags the use of destroy_all with a send receiver' do + inspect_source('foo.destroy_all # rubocop: disable DestroyAll') + + expect(cop.offenses.size).to eq(1) + end + + it 'flags the use of destroy_all with a constant receiver' do + inspect_source('User.destroy_all # rubocop: disable DestroyAll') + + expect(cop.offenses.size).to eq(1) + end + + it 'flags the use of destroy_all when passing arguments' do + inspect_source('User.destroy_all([])') + + expect(cop.offenses.size).to eq(1) + end + + it 'flags the use of destroy_all with a local variable receiver' do + inspect_source(<<~RUBY) + users = User.all + users.destroy_all # rubocop: disable DestroyAll + RUBY + + expect(cop.offenses.size).to eq(1) + end + + it 'does not flag the use of delete_all' do + inspect_source('foo.delete_all') + + expect(cop.offenses).to be_empty + end +end diff --git a/spec/rubocop/cop/migration/add_reference_spec.rb b/spec/rubocop/cop/migration/add_reference_spec.rb new file mode 100644 index 00000000000..8f795bb561e --- /dev/null +++ b/spec/rubocop/cop/migration/add_reference_spec.rb @@ -0,0 +1,54 @@ +require 'spec_helper' + +require 'rubocop' +require 'rubocop/rspec/support' + +require_relative '../../../../rubocop/cop/migration/add_reference' + +describe RuboCop::Cop::Migration::AddReference do + include CopHelper + + let(:cop) { described_class.new } + + context 'outside of a migration' do + it 'does not register any offenses' do + expect_no_offenses(<<~RUBY) + def up + add_reference(:projects, :users) + end + RUBY + end + end + + context 'in a migration' do + before do + allow(cop).to receive(:in_migration?).and_return(true) + end + + it 'registers an offense when using add_reference without index' do + expect_offense(<<~RUBY) + call do + add_reference(:projects, :users) + ^^^^^^^^^^^^^ `add_reference` requires `index: true` + end + RUBY + end + + it 'registers an offense when using add_reference index disabled' do + expect_offense(<<~RUBY) + def up + add_reference(:projects, :users, index: false) + ^^^^^^^^^^^^^ `add_reference` requires `index: true` + end + RUBY + end + + it 'does not register an offense when using add_reference with index enabled' do + expect_no_offenses(<<~RUBY) + def up + add_reference(:projects, :users, index: true) + end + RUBY + end + end +end diff --git a/spec/serializers/move_to_project_entity_spec.rb b/spec/serializers/move_to_project_entity_spec.rb new file mode 100644 index 00000000000..ac495eadb68 --- /dev/null +++ b/spec/serializers/move_to_project_entity_spec.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe MoveToProjectEntity do + describe '#as_json' do + let(:project) { build(:project, id: 1) } + + subject { described_class.new(project).as_json } + + it 'includes the project ID' do + expect(subject[:id]).to eq(project.id) + end + + it 'includes the full path' do + expect(subject[:name_with_namespace]).to eq(project.name_with_namespace) + end + end +end diff --git a/spec/serializers/move_to_project_serializer_spec.rb b/spec/serializers/move_to_project_serializer_spec.rb new file mode 100644 index 00000000000..841ac969eeb --- /dev/null +++ b/spec/serializers/move_to_project_serializer_spec.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe MoveToProjectSerializer do + describe '#represent' do + it 'includes the name and name with namespace' do + project = build(:project, id: 1) + output = described_class.new.represent(project) + + expect(output).to include(:id, :name_with_namespace) + end + end +end diff --git a/spec/serializers/project_mirror_serializer_spec.rb b/spec/serializers/project_mirror_serializer_spec.rb new file mode 100644 index 00000000000..5e47163532a --- /dev/null +++ b/spec/serializers/project_mirror_serializer_spec.rb @@ -0,0 +1,7 @@ +require 'spec_helper' + +describe ProjectMirrorSerializer do + it 'represents ProjectMirror entities' do + expect(described_class.entity_class).to eq(ProjectMirrorEntity) + end +end diff --git a/spec/services/ci/enqueue_build_service_spec.rb b/spec/services/ci/enqueue_build_service_spec.rb new file mode 100644 index 00000000000..e41b8e4800b --- /dev/null +++ b/spec/services/ci/enqueue_build_service_spec.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true +require 'spec_helper' + +describe Ci::EnqueueBuildService, '#execute' do + let(:user) { create(:user) } + let(:project) { create(:project) } + let(:ci_build) { create(:ci_build, :created) } + + subject { described_class.new(project, user).execute(ci_build) } + + it 'enqueues the build' do + subject + + expect(ci_build.pending?).to be_truthy + end +end diff --git a/spec/services/commits/tag_service_spec.rb b/spec/services/commits/tag_service_spec.rb new file mode 100644 index 00000000000..82377a8dace --- /dev/null +++ b/spec/services/commits/tag_service_spec.rb @@ -0,0 +1,102 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Commits::TagService do + let(:project) { create(:project, :repository) } + let(:user) { create(:user) } + + let(:commit) { project.commit } + + before do + project.add_maintainer(user) + end + + describe '#execute' do + let(:service) { described_class.new(project, user, opts) } + + shared_examples 'tag failure' do + it 'returns a hash with the :error status' do + result = service.execute(commit) + + expect(result[:status]).to eq(:error) + expect(result[:message]).to eq(error_message) + end + + it 'does not add a system note' do + service.execute(commit) + + description_notes = find_notes('tag') + expect(description_notes).to be_empty + end + end + + def find_notes(action) + commit + .notes + .joins(:system_note_metadata) + .where(system_note_metadata: { action: action }) + end + + context 'valid params' do + let(:opts) do + { + tag_name: 'v1.2.3', + tag_message: 'Release' + } + end + + def find_notes(action) + commit + .notes + .joins(:system_note_metadata) + .where(system_note_metadata: { action: action }) + end + + context 'when tagging succeeds' do + it 'returns a hash with the :success status and created tag' do + result = service.execute(commit) + + expect(result[:status]).to eq(:success) + + tag = result[:tag] + expect(tag.name).to eq(opts[:tag_name]) + expect(tag.message).to eq(opts[:tag_message]) + end + + it 'adds a system note' do + service.execute(commit) + + description_notes = find_notes('tag') + expect(description_notes.length).to eq(1) + end + end + + context 'when tagging fails' do + let(:tag_error) { 'GitLab: You are not allowed to push code to this project.' } + + before do + tag_stub = instance_double(Tags::CreateService) + allow(Tags::CreateService).to receive(:new).and_return(tag_stub) + allow(tag_stub).to receive(:execute).and_return({ + status: :error, message: tag_error + }) + end + + it_behaves_like 'tag failure' do + let(:error_message) { tag_error } + end + end + end + + context 'invalid params' do + let(:opts) do + {} + end + + it_behaves_like 'tag failure' do + let(:error_message) { 'Missing parameter tag_name' } + end + end + end +end diff --git a/spec/services/groups/destroy_service_spec.rb b/spec/services/groups/destroy_service_spec.rb index b54491cf5f9..d80d0f5a8a8 100644 --- a/spec/services/groups/destroy_service_spec.rb +++ b/spec/services/groups/destroy_service_spec.rb @@ -135,6 +135,17 @@ describe Groups::DestroyService do it_behaves_like 'group destruction', false end + context 'repository removal status is taken into account' do + it 'raises exception' do + expect_next_instance_of(::Projects::DestroyService) do |destroy_service| + expect(destroy_service).to receive(:execute).and_return(false) + end + + expect { destroy_group(group, user, false) } + .to raise_error(Groups::DestroyService::DestroyError, "Project #{project.id} can't be deleted" ) + end + end + describe 'repository removal' do before do destroy_group(group, user, false) diff --git a/spec/services/merge_requests/create_service_spec.rb b/spec/services/merge_requests/create_service_spec.rb index 06fb61baf33..74bcc15f912 100644 --- a/spec/services/merge_requests/create_service_spec.rb +++ b/spec/services/merge_requests/create_service_spec.rb @@ -134,9 +134,11 @@ describe MergeRequests::CreateService do let!(:pipeline_3) { create(:ci_pipeline, project: project, ref: "other_branch", project_id: project.id) } before do + # rubocop: disable DestroyAll project.merge_requests .where(source_branch: opts[:source_branch], target_branch: opts[:target_branch]) .destroy_all + # rubocop: enable DestroyAll end it 'sets head pipeline' do diff --git a/spec/services/merge_requests/delete_non_latest_diffs_service_spec.rb b/spec/services/merge_requests/delete_non_latest_diffs_service_spec.rb index 1c632847940..6268c149fc6 100644 --- a/spec/services/merge_requests/delete_non_latest_diffs_service_spec.rb +++ b/spec/services/merge_requests/delete_non_latest_diffs_service_spec.rb @@ -46,10 +46,12 @@ describe MergeRequests::DeleteNonLatestDiffsService, :clean_gitlab_redis_shared_ end it 'schedules no removal if there is no non-latest diffs' do + # rubocop: disable DestroyAll merge_request .merge_request_diffs .where.not(id: merge_request.latest_merge_request_diff_id) .destroy_all + # rubocop: enable DestroyAll expect(DeleteDiffFilesWorker).not_to receive(:bulk_perform_in) diff --git a/spec/services/notes/quick_actions_service_spec.rb b/spec/services/notes/quick_actions_service_spec.rb index 784dac55454..a8c994c101c 100644 --- a/spec/services/notes/quick_actions_service_spec.rb +++ b/spec/services/notes/quick_actions_service_spec.rb @@ -11,40 +11,6 @@ describe Notes::QuickActionsService do end end - shared_examples 'note on noteable that does not support quick actions' do - include_context 'note on noteable' - - before do - note.note = note_text - end - - describe 'note with only command' do - describe '/close, /label, /assign & /milestone' do - let(:note_text) { %(/close\n/assign @#{assignee.username}") } - - it 'saves the note and does not alter the note text' do - content, command_params = service.extract_commands(note) - - expect(content).to eq note_text - expect(command_params).to be_empty - end - end - end - - describe 'note with command & text' do - describe '/close, /label, /assign & /milestone' do - let(:note_text) { %(HELLO\n/close\n/assign @#{assignee.username}\nWORLD) } - - it 'saves the note and does not alter the note text' do - content, command_params = service.extract_commands(note) - - expect(content).to eq note_text - expect(command_params).to be_empty - end - end - end - end - shared_examples 'note on noteable that supports quick actions' do include_context 'note on noteable' @@ -147,16 +113,16 @@ describe Notes::QuickActionsService do expect(described_class.noteable_update_service(note)).to eq(Issues::UpdateService) end - it 'returns Issues::UpdateService for a note on a merge request' do + it 'returns MergeRequests::UpdateService for a note on a merge request' do note = create(:note_on_merge_request, project: project) expect(described_class.noteable_update_service(note)).to eq(MergeRequests::UpdateService) end - it 'returns nil for a note on a commit' do + it 'returns Commits::TagService for a note on a commit' do note = create(:note_on_commit, project: project) - expect(described_class.noteable_update_service(note)).to be_nil + expect(described_class.noteable_update_service(note)).to eq(Commits::TagService) end end @@ -175,7 +141,7 @@ describe Notes::QuickActionsService do let(:note) { create(:note_on_commit, project: project) } it 'returns false' do - expect(described_class.supported?(note)).to be_falsy + expect(described_class.supported?(note)).to be_truthy end end end @@ -203,10 +169,6 @@ describe Notes::QuickActionsService do it_behaves_like 'note on noteable that supports quick actions' do let(:note) { build(:note_on_merge_request, project: project) } end - - it_behaves_like 'note on noteable that does not support quick actions' do - let(:note) { build(:note_on_commit, project: project) } - end end context 'CE restriction for issue assignees' do diff --git a/spec/services/preview_markdown_service_spec.rb b/spec/services/preview_markdown_service_spec.rb index 81dc7c57f4a..507909d9231 100644 --- a/spec/services/preview_markdown_service_spec.rb +++ b/spec/services/preview_markdown_service_spec.rb @@ -65,6 +65,31 @@ describe PreviewMarkdownService do end end + context 'commit description' do + let(:project) { create(:project, :repository) } + let(:commit) { project.commit } + let(:params) do + { + text: "My work\n/tag v1.2.3 Stable release", + quick_actions_target_type: 'Commit', + quick_actions_target_id: commit.id + } + end + let(:service) { described_class.new(project, user, params) } + + it 'removes quick actions from text' do + result = service.execute + + expect(result[:text]).to eq 'My work' + end + + it 'explains quick actions effect' do + result = service.execute + + expect(result[:commands]).to eq 'Tags this commit to v1.2.3 with "Stable release".' + end + end + it 'sets correct markdown engine' do service = described_class.new(project, user, { markdown_version: CacheMarkdownField::CACHE_REDCARPET_VERSION }) result = service.execute diff --git a/spec/services/projects/detect_repository_languages_service_spec.rb b/spec/services/projects/detect_repository_languages_service_spec.rb index f90d558938f..deea1189cdf 100644 --- a/spec/services/projects/detect_repository_languages_service_spec.rb +++ b/spec/services/projects/detect_repository_languages_service_spec.rb @@ -5,10 +5,6 @@ describe Projects::DetectRepositoryLanguagesService, :clean_gitlab_redis_shared_ subject { described_class.new(project, project.owner) } - before do - allow(Feature).to receive(:disabled?).and_return(false) - end - describe '#execute' do context 'without previous detection' do it 'inserts new programming languages in the database' do diff --git a/spec/services/quick_actions/interpret_service_spec.rb b/spec/services/quick_actions/interpret_service_spec.rb index 743e281183e..bf1c157c4a2 100644 --- a/spec/services/quick_actions/interpret_service_spec.rb +++ b/spec/services/quick_actions/interpret_service_spec.rb @@ -6,6 +6,7 @@ describe QuickActions::InterpretService do let(:developer2) { create(:user) } let(:issue) { create(:issue, project: project) } let(:milestone) { create(:milestone, project: project, title: '9.10') } + let(:commit) { create(:commit, project: project) } let(:inprogress) { create(:label, project: project, title: 'In Progress') } let(:bug) { create(:label, project: project, title: 'Bug') } let(:note) { build(:note, commit_id: merge_request.diff_head_sha) } @@ -347,6 +348,14 @@ describe QuickActions::InterpretService do end end + shared_examples 'tag command' do + it 'tags a commit' do + _, updates = service.execute(content, issuable) + + expect(updates).to eq(tag_name: tag_name, tag_message: tag_message) + end + end + it_behaves_like 'reopen command' do let(:content) { '/reopen' } let(:issuable) { issue } @@ -628,16 +637,6 @@ describe QuickActions::InterpretService do let(:issuable) { merge_request } end - it_behaves_like 'todo command' do - let(:content) { '/todo' } - let(:issuable) { issue } - end - - it_behaves_like 'todo command' do - let(:content) { '/todo' } - let(:issuable) { merge_request } - end - it_behaves_like 'done command' do let(:content) { '/done' } let(:issuable) { issue } @@ -787,6 +786,28 @@ describe QuickActions::InterpretService do let(:issuable) { issue } end + context '/todo' do + let(:content) { '/todo' } + + context 'if issuable is an Issue' do + it_behaves_like 'todo command' do + let(:issuable) { issue } + end + end + + context 'if issuable is a MergeRequest' do + it_behaves_like 'todo command' do + let(:issuable) { merge_request } + end + end + + context 'if issuable is a Commit' do + it_behaves_like 'empty command' do + let(:issuable) { commit } + end + end + end + context '/copy_metadata command' do let(:todo_label) { create(:label, project: project, title: 'To Do') } let(:inreview_label) { create(:label, project: project, title: 'In Review') } @@ -971,6 +992,12 @@ describe QuickActions::InterpretService do let(:issuable) { issue } end end + + context 'if issuable is a Commit' do + let(:content) { '/award :100:' } + let(:issuable) { commit } + it_behaves_like 'empty command' + end end context '/shrug command' do @@ -1102,6 +1129,32 @@ describe QuickActions::InterpretService do it_behaves_like 'empty command' end end + + context '/tag command' do + let(:issuable) { commit } + + context 'ignores command with no argument' do + it_behaves_like 'empty command' do + let(:content) { '/tag' } + end + end + + context 'tags a commit with a tag name' do + it_behaves_like 'tag command' do + let(:tag_name) { 'v1.2.3' } + let(:tag_message) { nil } + let(:content) { "/tag #{tag_name}" } + end + end + + context 'tags a commit with a tag name and message' do + it_behaves_like 'tag command' do + let(:tag_name) { 'v1.2.3' } + let(:tag_message) { 'Stable release' } + let(:content) { "/tag #{tag_name} #{tag_message}" } + end + end + end end describe '#explain' do @@ -1319,5 +1372,39 @@ describe QuickActions::InterpretService do expect(explanations).to eq(["Moves this issue to test/project."]) end end + + describe 'tag a commit' do + describe 'with a tag name' do + context 'without a message' do + let(:content) { '/tag v1.2.3' } + + it 'includes the tag name only' do + _, explanations = service.explain(content, commit) + + expect(explanations).to eq(["Tags this commit to v1.2.3."]) + end + end + + context 'with an empty message' do + let(:content) { '/tag v1.2.3 ' } + + it 'includes the tag name only' do + _, explanations = service.explain(content, commit) + + expect(explanations).to eq(["Tags this commit to v1.2.3."]) + end + end + end + + describe 'with a tag name and message' do + let(:content) { '/tag v1.2.3 Stable release' } + + it 'includes the tag name and message' do + _, explanations = service.explain(content, commit) + + expect(explanations).to eq(["Tags this commit to v1.2.3 with \"Stable release\"."]) + end + end + end end end diff --git a/spec/services/quick_actions/target_service_spec.rb b/spec/services/quick_actions/target_service_spec.rb new file mode 100644 index 00000000000..0aeb29cbeec --- /dev/null +++ b/spec/services/quick_actions/target_service_spec.rb @@ -0,0 +1,83 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe QuickActions::TargetService do + let(:project) { create(:project) } + let(:user) { create(:user) } + let(:service) { described_class.new(project, user) } + + before do + project.add_maintainer(user) + end + + describe '#execute' do + shared_examples 'no target' do |type_id:| + it 'returns nil' do + target = service.execute(type, type_id) + + expect(target).to be_nil + end + end + + shared_examples 'find target' do + it 'returns the target' do + found_target = service.execute(type, target_id) + + expect(found_target).to eq(target) + end + end + + shared_examples 'build target' do |type_id:| + it 'builds a new target' do + target = service.execute(type, type_id) + + expect(target.project).to eq(project) + expect(target).to be_new_record + end + end + + context 'for issue' do + let(:target) { create(:issue, project: project) } + let(:target_id) { target.iid } + let(:type) { 'Issue' } + + it_behaves_like 'find target' + it_behaves_like 'build target', type_id: nil + it_behaves_like 'build target', type_id: -1 + end + + context 'for merge request' do + let(:target) { create(:merge_request, source_project: project) } + let(:target_id) { target.iid } + let(:type) { 'MergeRequest' } + + it_behaves_like 'find target' + it_behaves_like 'build target', type_id: nil + it_behaves_like 'build target', type_id: -1 + end + + context 'for commit' do + let(:project) { create(:project, :repository) } + let(:target) { project.commit.parent } + let(:target_id) { target.sha } + let(:type) { 'Commit' } + + it_behaves_like 'find target' + it_behaves_like 'no target', type_id: 'invalid_sha' + + context 'with nil target_id' do + let(:target) { project.commit } + let(:target_id) { nil } + + it_behaves_like 'find target' + end + end + + context 'for unknown type' do + let(:type) { 'unknown' } + + it_behaves_like 'no target', type_id: :unused + end + end +end diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb index 57d081cffb3..442de61f69b 100644 --- a/spec/services/system_note_service_spec.rb +++ b/spec/services/system_note_service_spec.rb @@ -108,6 +108,25 @@ describe SystemNoteService do end end + describe '.tag_commit' do + let(:noteable) do + project.commit + end + let(:tag_name) { 'v1.2.3' } + + subject { described_class.tag_commit(noteable, project, author, tag_name) } + + it_behaves_like 'a system note' do + let(:action) { 'tag' } + end + + it 'sets the note text' do + link = "http://localhost/#{project.full_path}/tags/#{tag_name}" + + expect(subject.note).to eq "tagged commit #{noteable.sha} to [`#{tag_name}`](#{link})" + end + end + describe '.change_assignee' do subject { described_class.change_assignee(noteable, project, author, assignee) } diff --git a/spec/services/todo_service_spec.rb b/spec/services/todo_service_spec.rb index 9a51c873b30..1746721b0d0 100644 --- a/spec/services/todo_service_spec.rb +++ b/spec/services/todo_service_spec.rb @@ -280,7 +280,7 @@ describe TodoService do end it 'does not create a todo if unassigned' do - issue.assignees.destroy_all + issue.assignees.destroy_all # rubocop: disable DestroyAll should_not_create_any_todo { service.reassigned_issue(issue, author) } end diff --git a/spec/services/users/destroy_service_spec.rb b/spec/services/users/destroy_service_spec.rb index 3bae8bfbd42..83f1495a1c6 100644 --- a/spec/services/users/destroy_service_spec.rb +++ b/spec/services/users/destroy_service_spec.rb @@ -20,7 +20,7 @@ describe Users::DestroyService do it 'will delete the project' do expect_next_instance_of(Projects::DestroyService) do |destroy_service| - expect(destroy_service).to receive(:execute).once + expect(destroy_service).to receive(:execute).once.and_return(true) end service.execute(user) @@ -35,7 +35,7 @@ describe Users::DestroyService do it 'destroys a project in pending_delete' do expect_next_instance_of(Projects::DestroyService) do |destroy_service| - expect(destroy_service).to receive(:execute).once + expect(destroy_service).to receive(:execute).once.and_return(true) end service.execute(user) @@ -172,23 +172,36 @@ describe Users::DestroyService do end describe "user personal's repository removal" do - before do - perform_enqueued_jobs { service.execute(user) } - end + context 'storages' do + before do + perform_enqueued_jobs { service.execute(user) } + end + + context 'legacy storage' do + let!(:project) { create(:project, :empty_repo, :legacy_storage, namespace: user.namespace) } + + it 'removes repository' do + expect(gitlab_shell.exists?(project.repository_storage, "#{project.disk_path}.git")).to be_falsey + end + end - context 'legacy storage' do - let!(:project) { create(:project, :empty_repo, :legacy_storage, namespace: user.namespace) } + context 'hashed storage' do + let!(:project) { create(:project, :empty_repo, namespace: user.namespace) } - it 'removes repository' do - expect(gitlab_shell.exists?(project.repository_storage, "#{project.disk_path}.git")).to be_falsey + it 'removes repository' do + expect(gitlab_shell.exists?(project.repository_storage, "#{project.disk_path}.git")).to be_falsey + end end end - context 'hashed storage' do - let!(:project) { create(:project, :empty_repo, namespace: user.namespace) } + context 'repository removal status is taken into account' do + it 'raises exception' do + expect_next_instance_of(::Projects::DestroyService) do |destroy_service| + expect(destroy_service).to receive(:execute).and_return(false) + end - it 'removes repository' do - expect(gitlab_shell.exists?(project.repository_storage, "#{project.disk_path}.git")).to be_falsey + expect { service.execute(user) } + .to raise_error(Users::DestroyService::DestroyError, "Project #{project.id} can't be deleted" ) end end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index bd564cc60a6..a15a46a9534 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -29,6 +29,7 @@ end # require rainbow gem String monkeypatch, so we can test SystemChecks require 'rainbow/ext/string' +Rainbow.enabled = false # Requires supporting ruby files with custom matchers and macros, etc, # in spec/support/ and its subdirectories. @@ -111,6 +112,13 @@ RSpec.configure do |config| config.before(:example) do # Enable all features by default for testing allow(Feature).to receive(:enabled?) { true } + + # The following can be removed when we remove the staged rollout strategy + # and we can just enable it using instance wide settings + # (ie. ApplicationSetting#auto_devops_enabled) + allow(Feature).to receive(:enabled?) + .with(:force_autodevops_on_by_default, anything) + .and_return(false) end config.before(:example, :request_store) do diff --git a/spec/support/api/milestones_shared_examples.rb b/spec/support/api/milestones_shared_examples.rb index a15189db35f..afd6448aa26 100644 --- a/spec/support/api/milestones_shared_examples.rb +++ b/spec/support/api/milestones_shared_examples.rb @@ -102,14 +102,6 @@ shared_examples_for 'group and project milestones' do |route_definition| expect(json_response['iid']).to eq(milestone.iid) end - it 'returns a milestone by id' do - get api(resource_route, user) - - expect(response).to have_gitlab_http_status(200) - expect(json_response['title']).to eq(milestone.title) - expect(json_response['iid']).to eq(milestone.iid) - end - it 'returns 401 error if user not authenticated' do get api(resource_route) diff --git a/spec/support/banzai/reference_filter_shared_examples.rb b/spec/support/banzai/reference_filter_shared_examples.rb index eb5da662ab5..476d80f3a93 100644 --- a/spec/support/banzai/reference_filter_shared_examples.rb +++ b/spec/support/banzai/reference_filter_shared_examples.rb @@ -11,3 +11,76 @@ shared_examples 'a reference containing an element node' do expect(doc.children.first.inner_html).to eq(inner_html) end end + +# Requires a reference, subject and subject_name: +# subject { create(:user) } +# let(:reference) { subject.to_reference } +# let(:subject_name) { 'user' } +shared_examples 'user reference or project reference' do + shared_examples 'it contains a data- attribute' do + it 'includes a data- attribute' do + doc = reference_filter("Hey #{reference}") + link = doc.css('a').first + + expect(link).to have_attribute("data-#{subject_name}") + expect(link.attr("data-#{subject_name}")).to eq subject.id.to_s + end + end + + context 'mentioning a resource' do + it_behaves_like 'a reference containing an element node' + it_behaves_like 'it contains a data- attribute' + + it "links to a resource" do + doc = reference_filter("Hey #{reference}") + expect(doc.css('a').first.attr('href')).to eq urls.send("#{subject_name}_url", subject) + end + + it 'links to a resource with a period' do + subject = create(subject_name.to_sym, name: 'alphA.Beta') + + doc = reference_filter("Hey #{get_reference(subject)}") + expect(doc.css('a').length).to eq 1 + end + + it 'links to a resource with an underscore' do + subject = create(subject_name.to_sym, name: 'ping_pong_king') + + doc = reference_filter("Hey #{get_reference(subject)}") + expect(doc.css('a').length).to eq 1 + end + + it 'links to a resource with different case-sensitivity' do + subject = create(subject_name.to_sym, name: 'RescueRanger') + reference = get_reference(subject) + + doc = reference_filter("Hey #{reference.upcase}") + expect(doc.css('a').length).to eq 1 + expect(doc.css('a').text).to eq(reference) + end + end + + it 'supports an :only_path context' do + doc = reference_filter("Hey #{reference}", only_path: true) + link = doc.css('a').first.attr('href') + + expect(link).not_to match %r(https?://) + expect(link).to eq urls.send "#{subject_name}_path", subject + end + + context 'referencing a resource in a link href' do + let(:reference) { %Q{<a href="#{get_reference(subject)}">Some text</a>} } + + it_behaves_like 'it contains a data- attribute' + + it 'links to the resource' do + doc = reference_filter("Hey #{reference}") + expect(doc.css('a').first.attr('href')).to eq urls.send "#{subject_name}_url", subject + end + + it 'links with adjacent text' do + doc = reference_filter("Mention me (#{reference}.)") + expect(doc.to_html).to match(%r{\(<a.+>Some text</a>\.\)}) + end + end +end diff --git a/spec/support/helpers/features/notes_helpers.rb b/spec/support/helpers/features/notes_helpers.rb index 2b9f8b30c60..89517fde6e2 100644 --- a/spec/support/helpers/features/notes_helpers.rb +++ b/spec/support/helpers/features/notes_helpers.rb @@ -20,6 +20,13 @@ module Spec end end end + + def preview_note(text) + page.within('.js-main-target-form') do + fill_in('note[note]', with: text) + click_on('Preview') + end + end end end end diff --git a/spec/support/helpers/test_env.rb b/spec/support/helpers/test_env.rb index f392660d2c7..21103771d1f 100644 --- a/spec/support/helpers/test_env.rb +++ b/spec/support/helpers/test_env.rb @@ -67,6 +67,7 @@ module TestEnv TMP_TEST_PATH = Rails.root.join('tmp', 'tests', '**') REPOS_STORAGE = 'default'.freeze + BROKEN_STORAGE = 'broken'.freeze # Test environment # @@ -157,10 +158,11 @@ module TestEnv component_timed_setup('Gitaly', install_dir: gitaly_dir, version: Gitlab::GitalyClient.expected_server_version, - task: "gitlab:gitaly:install[#{gitaly_dir}]") do + task: "gitlab:gitaly:install[#{gitaly_dir},#{repos_path}]") do - # Always re-create config, in case it's outdated. This is fast anyway. - Gitlab::SetupHelper.create_gitaly_configuration(gitaly_dir, force: true) + # Re-create config, to specify the broken storage path + storage_paths = { 'default' => repos_path, 'broken' => broken_path } + Gitlab::SetupHelper.create_gitaly_configuration(gitaly_dir, storage_paths, force: true) start_gitaly(gitaly_dir) end @@ -256,6 +258,10 @@ module TestEnv @repos_path ||= Gitlab.config.repositories.storages[REPOS_STORAGE].legacy_disk_path end + def broken_path + @broken_path ||= Gitlab.config.repositories.storages[BROKEN_STORAGE].legacy_disk_path + end + def backup_path Gitlab.config.backup.path end diff --git a/spec/support/import_export/configuration_helper.rb b/spec/support/import_export/configuration_helper.rb index bbac6ca6a9c..b4164cff922 100644 --- a/spec/support/import_export/configuration_helper.rb +++ b/spec/support/import_export/configuration_helper.rb @@ -9,7 +9,7 @@ module ConfigurationHelper end def relation_class_for_name(relation_name) - relation_name = Gitlab::ImportExport::RelationFactory::OVERRIDES[relation_name.to_sym] || relation_name + relation_name = Gitlab::ImportExport::RelationFactory.overrides[relation_name.to_sym] || relation_name Gitlab::ImportExport::RelationFactory.relation_class(relation_name) end diff --git a/spec/support/shared_examples/fast_destroy_all.rb b/spec/support/shared_examples/fast_destroy_all.rb index 5448ddcfe33..a8079b6d864 100644 --- a/spec/support/shared_examples/fast_destroy_all.rb +++ b/spec/support/shared_examples/fast_destroy_all.rb @@ -4,8 +4,8 @@ shared_examples_for 'fast destroyable' do expect(external_data_counter).to be > 0 expect(subjects.count).to be > 0 - expect { subjects.first.destroy }.to raise_error('`destroy` and `destroy_all` are forbbiden. Please use `fast_destroy_all`') - expect { subjects.destroy_all }.to raise_error('`destroy` and `destroy_all` are forbbiden. Please use `fast_destroy_all`') + expect { subjects.first.destroy }.to raise_error('`destroy` and `destroy_all` are forbidden. Please use `fast_destroy_all`') + expect { subjects.destroy_all }.to raise_error('`destroy` and `destroy_all` are forbidden. Please use `fast_destroy_all`') # rubocop: disable DestroyAll expect(subjects.count).to be > 0 expect(external_data_counter).to be > 0 diff --git a/spec/tasks/gitlab/gitaly_rake_spec.rb b/spec/tasks/gitlab/gitaly_rake_spec.rb index 4545226d78c..e6e4d9504d9 100644 --- a/spec/tasks/gitlab/gitaly_rake_spec.rb +++ b/spec/tasks/gitlab/gitaly_rake_spec.rb @@ -8,13 +8,23 @@ describe 'gitlab:gitaly namespace rake task' do describe 'install' do let(:repo) { 'https://gitlab.com/gitlab-org/gitaly.git' } let(:clone_path) { Rails.root.join('tmp/tests/gitaly').to_s } + let(:storage_path) { Rails.root.join('tmp/tests/repositories').to_s } let(:version) { File.read(Rails.root.join(Gitlab::GitalyClient::SERVER_VERSION_FILE)).chomp } + subject { run_rake_task('gitlab:gitaly:install', clone_path, storage_path) } + context 'no dir given' do it 'aborts and display a help message' do # avoid writing task output to spec progress allow($stderr).to receive :write - expect { run_rake_task('gitlab:gitaly:install') }.to raise_error /Please specify the directory where you want to install gitaly/ + expect { run_rake_task('gitlab:gitaly:install') }.to raise_error /Please specify the directory where you want to install gitaly and the path for the default storage/ + end + end + + context 'no storage path given' do + it 'aborts and display a help message' do + allow($stderr).to receive :write + expect { run_rake_task('gitlab:gitaly:install', clone_path) }.to raise_error /Please specify the directory where you want to install gitaly and the path for the default storage/ end end @@ -23,7 +33,7 @@ describe 'gitlab:gitaly namespace rake task' do expect(main_object) .to receive(:checkout_or_clone_version).and_raise 'Git error' - expect { run_rake_task('gitlab:gitaly:install', clone_path) }.to raise_error 'Git error' + expect { subject }.to raise_error 'Git error' end end @@ -36,7 +46,7 @@ describe 'gitlab:gitaly namespace rake task' do expect(main_object) .to receive(:checkout_or_clone_version).with(version: version, repo: repo, target_dir: clone_path) - run_rake_task('gitlab:gitaly:install', clone_path) + subject end end @@ -59,7 +69,7 @@ describe 'gitlab:gitaly namespace rake task' do expect(Gitlab::Popen).to receive(:popen).with(%w[which gmake]).and_return(['/usr/bin/gmake', 0]) expect(main_object).to receive(:run_command!).with(command_preamble + %w[gmake]).and_return(true) - run_rake_task('gitlab:gitaly:install', clone_path) + subject end end @@ -72,7 +82,7 @@ describe 'gitlab:gitaly namespace rake task' do it 'calls make in the gitaly directory' do expect(main_object).to receive(:run_command!).with(command_preamble + %w[make]).and_return(true) - run_rake_task('gitlab:gitaly:install', clone_path) + subject end context 'when Rails.env is test' do @@ -89,55 +99,10 @@ describe 'gitlab:gitaly namespace rake task' do it 'calls make in the gitaly directory with --no-deployment flag for bundle' do expect(main_object).to receive(:run_command!).with(command_preamble + command).and_return(true) - run_rake_task('gitlab:gitaly:install', clone_path) + subject end end end end end - - describe 'storage_config' do - it 'prints storage configuration in a TOML format' do - config = { - 'default' => Gitlab::GitalyClient::StorageSettings.new( - 'path' => '/path/to/default', - 'gitaly_address' => 'unix:/path/to/my.socket' - ), - 'nfs_01' => Gitlab::GitalyClient::StorageSettings.new( - 'path' => '/path/to/nfs_01', - 'gitaly_address' => 'unix:/path/to/my.socket' - ) - } - allow(Gitlab.config.repositories).to receive(:storages).and_return(config) - allow(Rails.env).to receive(:test?).and_return(false) - - expected_output = '' - Timecop.freeze do - expected_output = <<~TOML - # Gitaly storage configuration generated from #{Gitlab.config.source} on #{Time.current.to_s(:long)} - # This is in TOML format suitable for use in Gitaly's config.toml file. - bin_dir = "tmp/tests/gitaly" - socket_path = "/path/to/my.socket" - [gitlab-shell] - dir = "#{Gitlab.config.gitlab_shell.path}" - [[storage]] - name = "default" - path = "/path/to/default" - [[storage]] - name = "nfs_01" - path = "/path/to/nfs_01" - TOML - end - - expect { run_rake_task('gitlab:gitaly:storage_config')} - .to output(expected_output).to_stdout - - parsed_output = TomlRB.parse(expected_output) - config.each do |name, params| - Gitlab::GitalyClient::StorageSettings.allow_disk_access do - expect(parsed_output['storage']).to include({ 'name' => name, 'path' => params.legacy_disk_path }) - end - end - end - end end diff --git a/spec/tasks/gitlab/site_statistics_rake_spec.rb b/spec/tasks/gitlab/site_statistics_rake_spec.rb new file mode 100644 index 00000000000..20f0df65e63 --- /dev/null +++ b/spec/tasks/gitlab/site_statistics_rake_spec.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true +require 'rake_helper' + +describe 'rake gitlab:refresh_site_statistics' do + before do + Rake.application.rake_require 'tasks/gitlab/site_statistics' + + create(:project) + SiteStatistic.fetch.update(repositories_count: 0, wikis_count: 0) + end + + let(:task) { 'gitlab:refresh_site_statistics' } + + it 'recalculates existing counters' do + run_rake_task(task) + + expect(SiteStatistic.fetch.repositories_count).to eq(1) + expect(SiteStatistic.fetch.wikis_count).to eq(1) + end + + it 'displays message listing counters' do + expect { run_rake_task(task) }.to output(/Updating Site Statistics counters:.* Repositories\.\.\. OK!.* Wikis\.\.\. OK!/m).to_stdout + end +end diff --git a/spec/tasks/gitlab/traces_rake_spec.rb b/spec/tasks/gitlab/traces_rake_spec.rb index bd18e8ffc1e..aaf0d7242dd 100644 --- a/spec/tasks/gitlab/traces_rake_spec.rb +++ b/spec/tasks/gitlab/traces_rake_spec.rb @@ -5,51 +5,109 @@ describe 'gitlab:traces rake tasks' do Rake.application.rake_require 'tasks/gitlab/traces' end - shared_examples 'passes the job id to worker' do - it do - expect(ArchiveTraceWorker).to receive(:bulk_perform_async).with([[job.id]]) + describe 'gitlab:traces:archive' do + shared_examples 'passes the job id to worker' do + it do + expect(ArchiveTraceWorker).to receive(:bulk_perform_async).with([[job.id]]) - run_rake_task('gitlab:traces:archive') + run_rake_task('gitlab:traces:archive') + end end - end - shared_examples 'does not pass the job id to worker' do - it do - expect(ArchiveTraceWorker).not_to receive(:bulk_perform_async) + shared_examples 'does not pass the job id to worker' do + it do + expect(ArchiveTraceWorker).not_to receive(:bulk_perform_async) - run_rake_task('gitlab:traces:archive') + run_rake_task('gitlab:traces:archive') + end end - end - context 'when trace file stored in default path' do - let!(:job) { create(:ci_build, :success, :trace_live) } + context 'when trace file stored in default path' do + let!(:job) { create(:ci_build, :success, :trace_live) } - it_behaves_like 'passes the job id to worker' - end + it_behaves_like 'passes the job id to worker' + end - context 'when trace is stored in database' do - let!(:job) { create(:ci_build, :success) } + context 'when trace is stored in database' do + let!(:job) { create(:ci_build, :success) } - before do - job.update_column(:trace, 'trace in db') + before do + job.update_column(:trace, 'trace in db') + end + + it_behaves_like 'passes the job id to worker' end - it_behaves_like 'passes the job id to worker' + context 'when job has trace artifact' do + let!(:job) { create(:ci_build, :success) } + + before do + create(:ci_job_artifact, :trace, job: job) + end + + it_behaves_like 'does not pass the job id to worker' + end + + context 'when job is not finished yet' do + let!(:build) { create(:ci_build, :running, :trace_live) } + + it_behaves_like 'does not pass the job id to worker' + end end - context 'when job has trace artifact' do - let!(:job) { create(:ci_build, :success) } + describe 'gitlab:traces:migrate' do + let(:object_storage_enabled) { false } before do - create(:ci_job_artifact, :trace, job: job) + stub_artifacts_object_storage(enabled: object_storage_enabled) end - it_behaves_like 'does not pass the job id to worker' - end + subject { run_rake_task('gitlab:traces:migrate') } - context 'when job is not finished yet' do - let!(:build) { create(:ci_build, :running, :trace_live) } + let!(:job_trace) { create(:ci_job_artifact, :trace, file_store: store) } - it_behaves_like 'does not pass the job id to worker' + context 'when local storage is used' do + let(:store) { ObjectStorage::Store::LOCAL } + + context 'and job does not have file store defined' do + let(:object_storage_enabled) { true } + let(:store) { nil } + + it "migrates file to remote storage" do + subject + + expect(job_trace.reload.file_store).to eq(ObjectStorage::Store::REMOTE) + end + end + + context 'and remote storage is defined' do + let(:object_storage_enabled) { true } + + it "migrates file to remote storage" do + subject + + expect(job_trace.reload.file_store).to eq(ObjectStorage::Store::REMOTE) + end + end + + context 'and remote storage is not defined' do + it "fails to migrate to remote storage" do + subject + + expect(job_trace.reload.file_store).to eq(ObjectStorage::Store::LOCAL) + end + end + end + + context 'when remote storage is used' do + let(:object_storage_enabled) { true } + let(:store) { ObjectStorage::Store::REMOTE } + + it "file stays on remote storage" do + subject + + expect(job_trace.reload.file_store).to eq(ObjectStorage::Store::REMOTE) + end + end end end diff --git a/spec/views/shared/notes/_form.html.haml_spec.rb b/spec/views/shared/notes/_form.html.haml_spec.rb index c57319869f3..0189f926a5f 100644 --- a/spec/views/shared/notes/_form.html.haml_spec.rb +++ b/spec/views/shared/notes/_form.html.haml_spec.rb @@ -16,7 +16,7 @@ describe 'shared/notes/_form' do render end - %w[issue merge_request].each do |noteable| + %w[issue merge_request commit].each do |noteable| context "with a note on #{noteable}" do let(:note) { build(:"note_on_#{noteable}", project: project) } @@ -25,12 +25,4 @@ describe 'shared/notes/_form' do end end end - - context 'with a note on a commit' do - let(:note) { build(:note_on_commit, project: project) } - - it 'says that only markdown is supported, not quick actions' do - expect(rendered).to have_content('Markdown is supported') - end - end end diff --git a/spec/workers/project_destroy_worker_spec.rb b/spec/workers/project_destroy_worker_spec.rb index 42e1d86e3bb..6132f145f8d 100644 --- a/spec/workers/project_destroy_worker_spec.rb +++ b/spec/workers/project_destroy_worker_spec.rb @@ -18,13 +18,6 @@ describe ProjectDestroyWorker do expect(Dir.exist?(path)).to be_falsey end - it 'deletes the project but skips repo deletion' do - subject.perform(project.id, project.owner.id, { "skip_repo" => true }) - - expect(Project.all).not_to include(project) - expect(Dir.exist?(path)).to be_truthy - end - it 'does not raise error when project could not be found' do expect do subject.perform(-1, project.owner.id, {}) diff --git a/spec/workers/repository_check/single_repository_worker_spec.rb b/spec/workers/repository_check/single_repository_worker_spec.rb index 22fc64c1536..f11875cffd1 100644 --- a/spec/workers/repository_check/single_repository_worker_spec.rb +++ b/spec/workers/repository_check/single_repository_worker_spec.rb @@ -6,7 +6,7 @@ describe RepositoryCheck::SingleRepositoryWorker do it 'skips when the project has no push events' do project = create(:project, :repository, :wiki_disabled) - project.events.destroy_all + project.events.destroy_all # rubocop: disable DestroyAll break_project(project) expect(worker).not_to receive(:git_fsck) diff --git a/vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml b/vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml index b698248bc38..ffcf5648075 100644 --- a/vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml +++ b/vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml @@ -641,10 +641,10 @@ rollout 100%: function install_dependencies() { apk add -U openssl curl tar gzip bash ca-certificates git - wget -q -O /etc/apk/keys/sgerrand.rsa.pub https://raw.githubusercontent.com/sgerrand/alpine-pkg-glibc/master/sgerrand.rsa.pub - wget https://github.com/sgerrand/alpine-pkg-glibc/releases/download/2.23-r3/glibc-2.23-r3.apk - apk add glibc-2.23-r3.apk - rm glibc-2.23-r3.apk + wget -q -O /etc/apk/keys/sgerrand.rsa.pub https://alpine-pkgs.sgerrand.com/sgerrand.rsa.pub + wget https://github.com/sgerrand/alpine-pkg-glibc/releases/download/2.28-r0/glibc-2.28-r0.apk + apk add glibc-2.28-r0.apk + rm glibc-2.28-r0.apk curl "https://kubernetes-helm.storage.googleapis.com/helm-v${HELM_VERSION}-linux-amd64.tar.gz" | tar zx mv linux-amd64/helm /usr/bin/ @@ -720,10 +720,29 @@ rollout 100%: if [[ -f Dockerfile ]]; then echo "Building Dockerfile-based application..." - docker build -t "$CI_APPLICATION_REPOSITORY:$CI_APPLICATION_TAG" . + docker build \ + --build-arg HTTP_PROXY="$HTTP_PROXY" \ + --build-arg http_proxy="$http_proxy" \ + --build-arg HTTPS_PROXY="$HTTPS_PROXY" \ + --build-arg https_proxy="$https_proxy" \ + --build-arg FTP_PROXY="$FTP_PROXY" \ + --build-arg ftp_proxy="$ftp_proxy" \ + --build-arg NO_PROXY="$NO_PROXY" \ + --build-arg no_proxy="$no_proxy" \ + -t "$CI_APPLICATION_REPOSITORY:$CI_APPLICATION_TAG" . else echo "Building Heroku-based application using gliderlabs/herokuish docker image..." - docker run -i -e BUILDPACK_URL --name="$CI_CONTAINER_NAME" -v "$(pwd):/tmp/app:ro" gliderlabs/herokuish /bin/herokuish buildpack build + docker run -i \ + -e BUILDPACK_URL \ + -e HTTP_PROXY \ + -e http_proxy \ + -e HTTPS_PROXY \ + -e https_proxy \ + -e FTP_PROXY \ + -e ftp_proxy \ + -e NO_PROXY \ + -e no_proxy \ + --name="$CI_CONTAINER_NAME" -v "$(pwd):/tmp/app:ro" gliderlabs/herokuish /bin/herokuish buildpack build docker commit "$CI_CONTAINER_NAME" "$CI_APPLICATION_REPOSITORY:$CI_APPLICATION_TAG" docker rm "$CI_CONTAINER_NAME" >/dev/null echo "" diff --git a/vendor/gitlab-ci-yml/Pages/Middleman.gitlab-ci.yml b/vendor/gitlab-ci-yml/Pages/Middleman.gitlab-ci.yml index 9f4cc0574d6..983d7b5250e 100644 --- a/vendor/gitlab-ci-yml/Pages/Middleman.gitlab-ci.yml +++ b/vendor/gitlab-ci-yml/Pages/Middleman.gitlab-ci.yml @@ -1,24 +1,25 @@ # Full project: https://gitlab.com/pages/middleman -image: ruby:2.3 +image: ruby:2.4 +variables: + LANG: "C.UTF-8" cache: paths: - vendor -test: - script: +before_script: - apt-get update -yqqq - apt-get install -y nodejs - bundle install --path vendor + +test: + script: - bundle exec middleman build except: - master pages: script: - - apt-get update -yqqq - - apt-get install -y nodejs - - bundle install --path vendor - bundle exec middleman build artifacts: paths: diff --git a/vendor/licenses.csv b/vendor/licenses.csv index a462daf3067..ffe56286684 100644 --- a/vendor/licenses.csv +++ b/vendor/licenses.csv @@ -615,7 +615,6 @@ function-bind,1.1.1,MIT functional-red-black-tree,1.0.1,MIT fuzzaldrin-plus,0.5.0,MIT gauge,2.7.4,ISC -gemnasium-gitlab-service,0.2.6,MIT gemojione,3.3.0,MIT generate-function,2.0.0,MIT generate-object-property,1.2.0,MIT diff --git a/yarn.lock b/yarn.lock index c1e9d0ab73e..4326245d2ac 100644 --- a/yarn.lock +++ b/yarn.lock @@ -94,10 +94,34 @@ version "0.7.0" resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.7.0.tgz#9a06f4f137ee84d7df0460c1fdb1135ffa6c50fd" +"@types/events@*": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@types/events/-/events-1.2.0.tgz#81a6731ce4df43619e5c8c945383b3e62a89ea86" + +"@types/glob@^5": + version "5.0.35" + resolved "https://registry.yarnpkg.com/@types/glob/-/glob-5.0.35.tgz#1ae151c802cece940443b5ac246925c85189f32a" + dependencies: + "@types/events" "*" + "@types/minimatch" "*" + "@types/node" "*" + "@types/jquery@^2.0.40": version "2.0.48" resolved "https://registry.yarnpkg.com/@types/jquery/-/jquery-2.0.48.tgz#3e90d8cde2d29015e5583017f7830cb3975b2eef" +"@types/minimatch@*": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d" + +"@types/node@*": + version "10.5.2" + resolved "https://registry.yarnpkg.com/@types/node/-/node-10.5.2.tgz#f19f05314d5421fe37e74153254201a7bf00a707" + +"@types/parse5@^5": + version "5.0.0" + resolved "https://registry.yarnpkg.com/@types/parse5/-/parse5-5.0.0.tgz#9ae2106efc443d7c1e26570aa8247828c9c80f11" + "@vue/component-compiler-utils@^1.2.1": version "1.2.1" resolved "https://registry.yarnpkg.com/@vue/component-compiler-utils/-/component-compiler-utils-1.2.1.tgz#3d543baa75cfe5dab96e29415b78366450156ef6" @@ -2099,6 +2123,10 @@ css-loader@^1.0.0: postcss-value-parser "^3.3.0" source-list-map "^2.0.0" +css-selector-parser@^1.3: + version "1.3.0" + resolved "https://registry.yarnpkg.com/css-selector-parser/-/css-selector-parser-1.3.0.tgz#5f1ad43e2d8eefbfdc304fcd39a521664943e3eb" + css-selector-tokenizer@^0.7.0: version "0.7.0" resolved "https://registry.yarnpkg.com/css-selector-tokenizer/-/css-selector-tokenizer-0.7.0.tgz#e6988474ae8c953477bf5e7efecfceccd9cf4c86" @@ -3520,6 +3548,26 @@ getpass@^0.1.1: dependencies: assert-plus "^1.0.0" +gettext-extractor-vue@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/gettext-extractor-vue/-/gettext-extractor-vue-4.0.1.tgz#69d2737eb8f1938803ffcf9317133ed59fb2372f" + dependencies: + bluebird "^3.5.1" + glob "^7.1.2" + vue-template-compiler "^2.5.0" + +gettext-extractor@^3.3.2: + version "3.3.2" + resolved "https://registry.yarnpkg.com/gettext-extractor/-/gettext-extractor-3.3.2.tgz#d5172ba8d175678bd40a5abe7f908fa2a9d9473b" + dependencies: + "@types/glob" "^5" + "@types/parse5" "^5" + css-selector-parser "^1.3" + glob "5 - 7" + parse5 "^5" + pofile "^1" + typescript "^2" + glob-parent@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-3.1.0.tgz#9e6af6299d8d3bd2bd40430832bd113df906c5ae" @@ -3527,24 +3575,24 @@ glob-parent@^3.1.0: is-glob "^3.1.0" path-dirname "^1.0.0" -glob@^5.0.15: - version "5.0.15" - resolved "https://registry.yarnpkg.com/glob/-/glob-5.0.15.tgz#1bc936b9e02f4a603fcc222ecf7633d30b8b93b1" +"glob@5 - 7", glob@^7.0.3, glob@^7.0.5, glob@^7.1.1, glob@^7.1.2: + version "7.1.2" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15" dependencies: + fs.realpath "^1.0.0" inflight "^1.0.4" inherits "2" - minimatch "2 || 3" + minimatch "^3.0.4" once "^1.3.0" path-is-absolute "^1.0.0" -glob@^7.0.3, glob@^7.0.5, glob@^7.1.1, glob@^7.1.2: - version "7.1.2" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15" +glob@^5.0.15: + version "5.0.15" + resolved "https://registry.yarnpkg.com/glob/-/glob-5.0.15.tgz#1bc936b9e02f4a603fcc222ecf7633d30b8b93b1" dependencies: - fs.realpath "^1.0.0" inflight "^1.0.4" inherits "2" - minimatch "^3.0.4" + minimatch "2 || 3" once "^1.3.0" path-is-absolute "^1.0.0" @@ -5750,6 +5798,10 @@ parse-json@^2.2.0: dependencies: error-ex "^1.2.0" +parse5@^5: + version "5.0.0" + resolved "https://registry.yarnpkg.com/parse5/-/parse5-5.0.0.tgz#4d02710d44f3c3846197a11e205d4ef17842b81a" + parseqs@0.0.5: version "0.0.5" resolved "https://registry.yarnpkg.com/parseqs/-/parseqs-0.0.5.tgz#d5208a3738e46766e291ba2ea173684921a8b89d" @@ -5888,6 +5940,10 @@ pluralize@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-7.0.0.tgz#298b89df8b93b0221dbf421ad2b1b1ea23fc6777" +pofile@^1: + version "1.0.11" + resolved "https://registry.yarnpkg.com/pofile/-/pofile-1.0.11.tgz#35aff58c17491d127a07336d5522ebc9df57c954" + popper.js@^1.12.9, popper.js@^1.14.3: version "1.14.3" resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.14.3.tgz#1438f98d046acf7b4d78cd502bf418ac64d4f095" @@ -7460,6 +7516,10 @@ typedarray@^0.0.6: version "0.0.6" resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" +typescript@^2: + version "2.9.2" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.9.2.tgz#1cbf61d05d6b96269244eb6a3bce4bd914e0f00c" + uglify-es@^3.3.4: version "3.3.9" resolved "https://registry.yarnpkg.com/uglify-es/-/uglify-es-3.3.9.tgz#0c1c4f0700bed8dbc124cdb304d2592ca203e677" @@ -7756,7 +7816,7 @@ vue-style-loader@^4.1.0: hash-sum "^1.0.2" loader-utils "^1.0.2" -vue-template-compiler@^2.5.16: +vue-template-compiler@^2.5.0, vue-template-compiler@^2.5.16: version "2.5.16" resolved "https://registry.yarnpkg.com/vue-template-compiler/-/vue-template-compiler-2.5.16.tgz#93b48570e56c720cdf3f051cc15287c26fbd04cb" dependencies: |